├── .gitattributes ├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── eevee ├── __init__.py ├── __main__.py ├── cogs │ ├── __init__.py │ ├── community │ │ ├── __init__.py │ │ └── cog.py │ ├── dev │ │ ├── __init__.py │ │ ├── cog.py │ │ └── unknown_events.py │ ├── google │ │ ├── __init__.py │ │ └── google_cog.py │ ├── pokemon │ │ ├── __init__.py │ │ ├── cog.py │ │ ├── errors.py │ │ └── objects.py │ ├── public_tests │ │ ├── __init__.py │ │ └── cog.py │ ├── stats │ │ ├── __init__.py │ │ └── cog.py │ ├── tests │ │ ├── __init__.py │ │ ├── cog.py │ │ └── tables.py │ ├── time │ │ ├── __init__.py │ │ └── cog.py │ ├── trainers │ │ ├── __init__.py │ │ ├── cog.py │ │ ├── objects.py │ │ └── tables.py │ └── xkcd │ │ ├── __init__.py │ │ ├── cog.py │ │ └── tables.py ├── config_template.py ├── core │ ├── __init__.py │ ├── bot.py │ ├── checks.py │ ├── cog_base.py │ ├── cog_manager.py │ ├── commands.py │ ├── context.py │ ├── data_manager │ │ ├── __init__.py │ │ ├── dbi.py │ │ ├── errors.py │ │ ├── guild.py │ │ ├── manager.py │ │ ├── schema.py │ │ ├── sqltypes.py │ │ └── tables.py │ ├── error_handling.py │ ├── errors.py │ └── logger.py ├── data │ ├── permissions.json │ ├── pkmn_info.json │ ├── pkmn_info_new.json │ ├── pokedex-temp.sqlite │ └── raid_info.json ├── launcher.py └── utils │ ├── __init__.py │ ├── converters.py │ ├── cvtest.py │ ├── datatypes.py │ ├── enums.py │ ├── formatters.py │ ├── fuzzymatch.py │ ├── i18n.py │ ├── pagination.py │ └── snowflake.py └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | bin/ 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | .vscode/ 23 | eevee/config.py 24 | docs/ 25 | logs/ 26 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | "discord.py" = {git = "https://github.com/Rapptz/discord.py",ref = "3f06f24"} 8 | python-dateutil = ">=2.6" 9 | asyncpg = ">=0.13" 10 | python-levenshtein = ">=0.12" 11 | fuzzywuzzy = "*" 12 | psutil = "*" 13 | aiocontextvars = "*" 14 | colorthief = "*" 15 | more-itertools = "*" 16 | numpy = "*" 17 | pendulum = "*" 18 | pytz = "*" 19 | matplotlib = "*" 20 | async_timeout = "~=3.0" 21 | 22 | [requires] 23 | python_version = "3.6" 24 | 25 | [scripts] 26 | bot = "python -m eevee" 27 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "f94b0f051bb554fa935e35197f05b9c503e06fff12170f95af1d13605298ec56" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "aiocontextvars": { 20 | "hashes": [ 21 | "sha256:885daf8261818767d8f7cbd79f9d4482d118f024b6586ef6e67980236a27bfa3", 22 | "sha256:f027372dc48641f683c559f247bd84962becaacdc9ba711d583c3871fb5652aa" 23 | ], 24 | "index": "pypi", 25 | "version": "==0.2.2" 26 | }, 27 | "async-timeout": { 28 | "hashes": [ 29 | "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", 30 | "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" 31 | ], 32 | "index": "pypi", 33 | "version": "==3.0.1" 34 | }, 35 | "asyncpg": { 36 | "hashes": [ 37 | "sha256:0677714b26b48d63db728867b812ef365ec3879d2be6fa1c9cf4328503f9a464", 38 | "sha256:2dee4fb251139f1c1ee4bd9959d516f930f4da37a2f33b07c2b902b837a76666", 39 | "sha256:378a7ef11ce7b35f11eb816e5252bc1e779119f7583a872233b45a76effac02e", 40 | "sha256:4539bc2e63600a1ee999086bbb59bf717ab32ea771ac20b5b792a2234633b5fb", 41 | "sha256:4a779a85302241782bed8ed0f2bcb38544805b3e107b16ee7489c5818d8f4228", 42 | "sha256:51a3d67a3fa43112b17ec510338723932e1e0611ad99a146acc9960d32210196", 43 | "sha256:58a5eccaac60fd326e32683226efe1046bfea558fa043360bdd1708e0e812c67", 44 | "sha256:814343dc2baa489a11521ff9fad68f337a05c9ae0461fdf9f1ec7ac3541c13a9", 45 | "sha256:84084f7dfed0b2d397a0c2fd7eaf29b01904c74f4320e5fe95ad3042042cf188", 46 | "sha256:89e727fdba05d90a0156d9d18932fd44a2baa84e90e3368573f432a308ad8fd7", 47 | "sha256:ab8b9d367e3ef48f35a059642940714a2bda7a7fce8b017b21bfbc4f8fbf8f5f", 48 | "sha256:c1fe1f0ef848f0f17bf63b90a4c3f446a14e4c899d8531ea988109cc0de014e5", 49 | "sha256:cc7aa61bf41273ee5d4c11e0e72c0d9340e9c4dbf752464ae2b6816abadaabce", 50 | "sha256:d5450bdf8631fa1200c08a2e70cab06c2e8c09ef608629908531513444d12858", 51 | "sha256:fd2d13da29f55c2c71b1acc9d9f107c7a5176fffb3f62ff503f2b300f7ecd74e", 52 | "sha256:fd35a8082b97d5b97d26bcd1b010fdd65a56311d7a02bf2a7e2c56810b9961a7" 53 | ], 54 | "index": "pypi", 55 | "version": "==0.18.3" 56 | }, 57 | "colorthief": { 58 | "hashes": [ 59 | "sha256:079cb0c95bdd669c4643e2f7494de13b0b6029d5cdbe2d74d5d3c3386bd57221", 60 | "sha256:b04fc8ce5cf9c888768745e29cb19b7b688d5711af6fba26e8057debabec56b9" 61 | ], 62 | "index": "pypi", 63 | "version": "==0.2.1" 64 | }, 65 | "cycler": { 66 | "hashes": [ 67 | "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d", 68 | "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8" 69 | ], 70 | "version": "==0.10.0" 71 | }, 72 | "discord-py": { 73 | "git": "https://github.com/Rapptz/discord.py", 74 | "ref": "3f06f247c039a23948e7bb0014ea31db533b4ba2" 75 | }, 76 | "discord.py": { 77 | "git": "https://github.com/Rapptz/discord.py", 78 | "ref": "3f06f24" 79 | }, 80 | "fuzzywuzzy": { 81 | "hashes": [ 82 | "sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254", 83 | "sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62" 84 | ], 85 | "index": "pypi", 86 | "version": "==0.17.0" 87 | }, 88 | "kiwisolver": { 89 | "hashes": [ 90 | "sha256:05b5b061e09f60f56244adc885c4a7867da25ca387376b02c1efc29cc16bcd0f", 91 | "sha256:26f4fbd6f5e1dabff70a9ba0d2c4bd30761086454aa30dddc5b52764ee4852b7", 92 | "sha256:3b2378ad387f49cbb328205bda569b9f87288d6bc1bf4cd683c34523a2341efe", 93 | "sha256:400599c0fe58d21522cae0e8b22318e09d9729451b17ee61ba8e1e7c0346565c", 94 | "sha256:47b8cb81a7d18dbaf4fed6a61c3cecdb5adec7b4ac292bddb0d016d57e8507d5", 95 | "sha256:53eaed412477c836e1b9522c19858a8557d6e595077830146182225613b11a75", 96 | "sha256:58e626e1f7dfbb620d08d457325a4cdac65d1809680009f46bf41eaf74ad0187", 97 | "sha256:5a52e1b006bfa5be04fe4debbcdd2688432a9af4b207a3f429c74ad625022641", 98 | "sha256:5c7ca4e449ac9f99b3b9d4693debb1d6d237d1542dd6a56b3305fe8a9620f883", 99 | "sha256:682e54f0ce8f45981878756d7203fd01e188cc6c8b2c5e2cf03675390b4534d5", 100 | "sha256:79bfb2f0bd7cbf9ea256612c9523367e5ec51d7cd616ae20ca2c90f575d839a2", 101 | "sha256:7f4dd50874177d2bb060d74769210f3bce1af87a8c7cf5b37d032ebf94f0aca3", 102 | "sha256:8944a16020c07b682df861207b7e0efcd2f46c7488619cb55f65882279119389", 103 | "sha256:8aa7009437640beb2768bfd06da049bad0df85f47ff18426261acecd1cf00897", 104 | "sha256:939f36f21a8c571686eb491acfffa9c7f1ac345087281b412d63ea39ca14ec4a", 105 | "sha256:9733b7f64bd9f807832d673355f79703f81f0b3e52bfce420fc00d8cb28c6a6c", 106 | "sha256:a02f6c3e229d0b7220bd74600e9351e18bc0c361b05f29adae0d10599ae0e326", 107 | "sha256:a0c0a9f06872330d0dd31b45607197caab3c22777600e88031bfe66799e70bb0", 108 | "sha256:acc4df99308111585121db217681f1ce0eecb48d3a828a2f9bbf9773f4937e9e", 109 | "sha256:b64916959e4ae0ac78af7c3e8cef4becee0c0e9694ad477b4c6b3a536de6a544", 110 | "sha256:d3fcf0819dc3fea58be1fd1ca390851bdb719a549850e708ed858503ff25d995", 111 | "sha256:d52e3b1868a4e8fd18b5cb15055c76820df514e26aa84cc02f593d99fef6707f", 112 | "sha256:db1a5d3cc4ae943d674718d6c47d2d82488ddd94b93b9e12d24aabdbfe48caee", 113 | "sha256:e3a21a720791712ed721c7b95d433e036134de6f18c77dbe96119eaf7aa08004", 114 | "sha256:e8bf074363ce2babeb4764d94f8e65efd22e6a7c74860a4f05a6947afc020ff2", 115 | "sha256:f16814a4a96dc04bf1da7d53ee8d5b1d6decfc1a92a63349bb15d37b6a263dd9", 116 | "sha256:f2b22153870ca5cf2ab9c940d7bc38e8e9089fa0f7e5856ea195e1cf4ff43d5a", 117 | "sha256:f790f8b3dff3d53453de6a7b7ddd173d2e020fb160baff578d578065b108a05f" 118 | ], 119 | "version": "==1.1.0" 120 | }, 121 | "matplotlib": { 122 | "hashes": [ 123 | "sha256:08d9bc2e2acef42965256acd5015dc2c899cbd53e01bf4214c5510c7ea0efd2d", 124 | "sha256:1e0213f87cc0076f7b0c4c251d7e23601e2419cd98691df79edb95517ba06f0c", 125 | "sha256:1f31053f660df5f0310118d7f5bd1e8025170e9773f0bebe8fec486d0926adf6", 126 | "sha256:399bf6352633aeeb45ca55c6c943fa2738022fb17ae498c32a142ced0b41528d", 127 | "sha256:409a5894efb810d630d2512449c7a4394de9a4d15fc6394e26a409b17d9cc18c", 128 | "sha256:5c5ef5cf1bc8f483123102e2615644937af7d4c01d100acc72bf74a044a78717", 129 | "sha256:d0052be5cdfa27018bb08194b8812c47cb985d60eb682e1809c76e9600839516", 130 | "sha256:e7d6620d145ca9f6c3e88248e5734b6fda430e75e70755b887e48f8e9bc1de2a", 131 | "sha256:f3d8b6bccc577e4e5ecbd58fdd63cacb8e58f0ed1e97616a7f7a7baaf4b8d036" 132 | ], 133 | "index": "pypi", 134 | "version": "==3.1.0" 135 | }, 136 | "more-itertools": { 137 | "hashes": [ 138 | "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", 139 | "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a" 140 | ], 141 | "index": "pypi", 142 | "version": "==7.0.0" 143 | }, 144 | "numpy": { 145 | "hashes": [ 146 | "sha256:0e2eed77804b2a6a88741f8fcac02c5499bba3953ec9c71e8b217fad4912c56c", 147 | "sha256:1c666f04553ef70fda54adf097dbae7080645435fc273e2397f26bbf1d127bbb", 148 | "sha256:1f46532afa7b2903bfb1b79becca2954c0a04389d19e03dc73f06b039048ac40", 149 | "sha256:315fa1b1dfc16ae0f03f8fd1c55f23fd15368710f641d570236f3d78af55e340", 150 | "sha256:3d5fcea4f5ed40c3280791d54da3ad2ecf896f4c87c877b113576b8280c59441", 151 | "sha256:48241759b99d60aba63b0e590332c600fc4b46ad597c9b0a53f350b871ef0634", 152 | "sha256:4b4f2924b36d857cf302aec369caac61e43500c17eeef0d7baacad1084c0ee84", 153 | "sha256:54fe3b7ed9e7eb928bbc4318f954d133851865f062fa4bbb02ef8940bc67b5d2", 154 | "sha256:5a8f021c70e6206c317974c93eaaf9bc2b56295b6b1cacccf88846e44a1f33fc", 155 | "sha256:754a6be26d938e6ca91942804eb209307b73f806a1721176278a6038869a1686", 156 | "sha256:771147e654e8b95eea1293174a94f34e2e77d5729ad44aefb62fbf8a79747a15", 157 | "sha256:78a6f89da87eeb48014ec652a65c4ffde370c036d780a995edaeb121d3625621", 158 | "sha256:7fde5c2a3a682a9e101e61d97696687ebdba47637611378b4127fe7e47fdf2bf", 159 | "sha256:80d99399c97f646e873dd8ce87c38cfdbb668956bbc39bc1e6cac4b515bba2a0", 160 | "sha256:88a72c1e45a0ae24d1f249a529d9f71fe82e6fa6a3fd61414b829396ec585900", 161 | "sha256:a4f4460877a16ac73302a9c077ca545498d9fe64e6a81398d8e1a67e4695e3df", 162 | "sha256:a61255a765b3ac73ee4b110b28fccfbf758c985677f526c2b4b39c48cc4b509d", 163 | "sha256:ab4896a8c910b9a04c0142871d8800c76c8a2e5ff44763513e1dd9d9631ce897", 164 | "sha256:abbd6b1c2ef6199f4b7ca9f818eb6b31f17b73a6110aadc4e4298c3f00fab24e", 165 | "sha256:b16d88da290334e33ea992c56492326ea3b06233a00a1855414360b77ca72f26", 166 | "sha256:b78a1defedb0e8f6ae1eb55fa6ac74ab42acc4569c3a2eacc2a407ee5d42ebcb", 167 | "sha256:cfef82c43b8b29ca436560d51b2251d5117818a8d1fb74a8384a83c096745dad", 168 | "sha256:d160e57731fcdec2beda807ebcabf39823c47e9409485b5a3a1db3a8c6ce763e" 169 | ], 170 | "index": "pypi", 171 | "version": "==1.16.3" 172 | }, 173 | "pendulum": { 174 | "hashes": [ 175 | "sha256:0f43d963b27e92b04047ce8352e4c277db99f20d0b513df7d0ceafe674a2f727", 176 | "sha256:14e60d26d7400980123dbb6e3f2a90b70d7c18c63742ffe5bd6d6a643f8c6ef1", 177 | "sha256:5035a4e17504814a679f138374269cc7cc514aeac7ba6d9dc020abc224f25dbc", 178 | "sha256:8c0b3d655c1e9205d4dacf42fffc929cde3b19b5fb544a7f7561e6896eb8a000", 179 | "sha256:bfc7b33ae193a204ec0bec12ad0d2d3300cd7e51d91d992da525ba3b28f0d265", 180 | "sha256:cd70b75800439794e1ad8dbfa24838845e171918df81fa98b68d0d5a6f9b8bf2", 181 | "sha256:cf535d36c063575d4752af36df928882b2e0e31541b4482c97d63752785f9fcb" 182 | ], 183 | "index": "pypi", 184 | "version": "==2.0.4" 185 | }, 186 | "pillow": { 187 | "hashes": [ 188 | "sha256:15c056bfa284c30a7f265a41ac4cbbc93bdbfc0dfe0613b9cb8a8581b51a9e55", 189 | "sha256:1a4e06ba4f74494ea0c58c24de2bb752818e9d504474ec95b0aa94f6b0a7e479", 190 | "sha256:1c3c707c76be43c9e99cb7e3d5f1bee1c8e5be8b8a2a5eeee665efbf8ddde91a", 191 | "sha256:1fd0b290203e3b0882d9605d807b03c0f47e3440f97824586c173eca0aadd99d", 192 | "sha256:24114e4a6e1870c5a24b1da8f60d0ba77a0b4027907860188ea82bd3508c80eb", 193 | "sha256:258d886a49b6b058cd7abb0ab4b2b85ce78669a857398e83e8b8e28b317b5abb", 194 | "sha256:33c79b6dd6bc7f65079ab9ca5bebffb5f5d1141c689c9c6a7855776d1b09b7e8", 195 | "sha256:367385fc797b2c31564c427430c7a8630db1a00bd040555dfc1d5c52e39fcd72", 196 | "sha256:3c1884ff078fb8bf5f63d7d86921838b82ed4a7d0c027add773c2f38b3168754", 197 | "sha256:44e5240e8f4f8861d748f2a58b3f04daadab5e22bfec896bf5434745f788f33f", 198 | "sha256:46aa988e15f3ea72dddd81afe3839437b755fffddb5e173886f11460be909dce", 199 | "sha256:74d90d499c9c736d52dd6d9b7221af5665b9c04f1767e35f5dd8694324bd4601", 200 | "sha256:809c0a2ce9032cbcd7b5313f71af4bdc5c8c771cb86eb7559afd954cab82ebb5", 201 | "sha256:85d1ef2cdafd5507c4221d201aaf62fc9276f8b0f71bd3933363e62a33abc734", 202 | "sha256:8c3889c7681af77ecfa4431cd42a2885d093ecb811e81fbe5e203abc07e0995b", 203 | "sha256:9218d81b9fca98d2c47d35d688a0cea0c42fd473159dfd5612dcb0483c63e40b", 204 | "sha256:9aa4f3827992288edd37c9df345783a69ef58bd20cc02e64b36e44bcd157bbf1", 205 | "sha256:9d80f44137a70b6f84c750d11019a3419f409c944526a95219bea0ac31f4dd91", 206 | "sha256:b7ebd36128a2fe93991293f997e44be9286503c7530ace6a55b938b20be288d8", 207 | "sha256:c4c78e2c71c257c136cdd43869fd3d5e34fc2162dc22e4a5406b0ebe86958239", 208 | "sha256:c6a842537f887be1fe115d8abb5daa9bc8cc124e455ff995830cc785624a97af", 209 | "sha256:cf0a2e040fdf5a6d95f4c286c6ef1df6b36c218b528c8a9158ec2452a804b9b8", 210 | "sha256:cfd28aad6fc61f7a5d4ee556a997dc6e5555d9381d1390c00ecaf984d57e4232", 211 | "sha256:dca5660e25932771460d4688ccbb515677caaf8595f3f3240ec16c117deff89a", 212 | "sha256:de7aedc85918c2f887886442e50f52c1b93545606317956d65f342bd81cb4fc3", 213 | "sha256:e6c0bbf8e277b74196e3140c35f9a1ae3eafd818f7f2d3a15819c49135d6c062" 214 | ], 215 | "version": "==6.0.0" 216 | }, 217 | "psutil": { 218 | "hashes": [ 219 | "sha256:206eb909aa8878101d0eca07f4b31889c748f34ed6820a12eb3168c7aa17478e", 220 | "sha256:649f7ffc02114dced8fbd08afcd021af75f5f5b2311bc0e69e53e8f100fe296f", 221 | "sha256:6ebf2b9c996bb8c7198b385bade468ac8068ad8b78c54a58ff288cd5f61992c7", 222 | "sha256:753c5988edc07da00dafd6d3d279d41f98c62cd4d3a548c4d05741a023b0c2e7", 223 | "sha256:76fb0956d6d50e68e3f22e7cc983acf4e243dc0fcc32fd693d398cb21c928802", 224 | "sha256:828e1c3ca6756c54ac00f1427fdac8b12e21b8a068c3bb9b631a1734cada25ed", 225 | "sha256:a4c62319ec6bf2b3570487dd72d471307ae5495ce3802c1be81b8a22e438b4bc", 226 | "sha256:acba1df9da3983ec3c9c963adaaf530fcb4be0cd400a8294f1ecc2db56499ddd", 227 | "sha256:ef342cb7d9b60e6100364f50c57fa3a77d02ff8665d5b956746ac01901247ac4" 228 | ], 229 | "index": "pypi", 230 | "version": "==5.6.2" 231 | }, 232 | "pyparsing": { 233 | "hashes": [ 234 | "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", 235 | "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" 236 | ], 237 | "version": "==2.4.0" 238 | }, 239 | "python-dateutil": { 240 | "hashes": [ 241 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 242 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 243 | ], 244 | "index": "pypi", 245 | "version": "==2.8.0" 246 | }, 247 | "python-levenshtein": { 248 | "hashes": [ 249 | "sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1" 250 | ], 251 | "index": "pypi", 252 | "version": "==0.12.0" 253 | }, 254 | "pytz": { 255 | "hashes": [ 256 | "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", 257 | "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" 258 | ], 259 | "index": "pypi", 260 | "version": "==2019.1" 261 | }, 262 | "pytzdata": { 263 | "hashes": [ 264 | "sha256:778db26940e38cf6547d6574f49375570f7d697970461de531c56cf8400958a3", 265 | "sha256:f0469062f799c66480fcc7eae69a8270dc83f0e6522c0e70db882d6bd708d378" 266 | ], 267 | "version": "==2019.1" 268 | }, 269 | "six": { 270 | "hashes": [ 271 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 272 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 273 | ], 274 | "version": "==1.12.0" 275 | } 276 | }, 277 | "develop": {} 278 | } 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eevee v2 - In Developement 2 | A Discord helper bot for Pokemon Go communities. 3 | 4 | Eevee is a Pokemon Go community manager and coordinator bot for Discord servers, upholding the ideal to respect the Terms of Service of both the game and the associated accounts. 5 | 6 | It is written in Python 3.6.1 using the [discord.py v1.0.0a](https://github.com/Rapptz/discord.py/tree/rewrite) library. 7 | 8 | This version of Eevee is currently under heavy development and is not recommended for any type of actual usage. 9 | 10 | This update aims to meet the following goals: 11 | - [x] Compatible with [discord.py v1.0.0a](https://github.com/Rapptz/discord.py/tree/rewrite) 12 | - [x] Able to be installed as a pip package with dependancies auto-installing 13 | - [x] Redesign Meowth into a modular structure, with extension management 14 | - [x] Ability to update most of the codebase with no downtime or loss of data 15 | - [x] Able to perform all current features of Eevee v1 or excel beyond it 16 | 17 | ## Proposed Structure: 18 | - Docs 19 | - Readme 20 | - Setup 21 | - Meowth 22 | - Core 23 | - Config 24 | - Data Management 25 | - Cogs 26 | - Server Greeting 27 | - Team Management 28 | - Wild Tracking 29 | - Raid Management 30 | - Gym Management 31 | -------------------------------------------------------------------------------- /eevee/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """A Pokemon Go Community Bot for Discord. 3 | 4 | Eevee is a Discord bot written in Python 3.6.1 using version 1.0.0a of the discord.py library. 5 | It assists with the organisation of local Pokemon Go Discord servers and their members.""" 6 | 7 | __version__ = "2.0.0a0" 8 | 9 | __author__ = "scragly" 10 | __credits__ = ["FoglyOgly"] 11 | __maintainer__ = "scragly" 12 | __status__ = "Development" 13 | 14 | from eevee.core.bot import command, group 15 | from eevee.core.cog_base import Cog 16 | from eevee.core import checks, errors 17 | -------------------------------------------------------------------------------- /eevee/__main__.py: -------------------------------------------------------------------------------- 1 | """Eevee Bot Module. 2 | 3 | By using this command instead of ``eevee``, the bot will bypass the launcher. 4 | 5 | Command: 6 | ``eevee-bot`` 7 | 8 | Options: 9 | -d, --debug Enable debug mode. 10 | """ 11 | import argparse 12 | import sys 13 | 14 | import discord 15 | 16 | from eevee.core import bot, logger 17 | from eevee.utils import ExitCodes 18 | 19 | if discord.version_info.major < 1: 20 | print("You are not running discord.py v1.0.0a or above.\n\n" 21 | "Eevee v2 requires the new discord.py library to function " 22 | "correctly. Please install the correct version.") 23 | sys.exit(1) 24 | 25 | def run_eevee(debug=False, launcher=None, from_restart=False): 26 | """Sets up the bot, runs it and handles exit codes.""" 27 | 28 | # create bot instance 29 | description = "Eevee v2 - Alpha" 30 | eevee = bot.Eevee( 31 | description=description, launcher=launcher, 32 | debug=debug, from_restart=from_restart) 33 | 34 | # setup logging 35 | eevee.logger = logger.init_logger(eevee, debug) 36 | 37 | # load the required core extensions 38 | eevee.load_extension('eevee.core.error_handling') 39 | eevee.load_extension('eevee.core.commands') 40 | eevee.load_extension('eevee.core.cog_manager') 41 | 42 | # load extensions marked for preload in config 43 | for ext in eevee.preload_ext: 44 | ext_name = ("eevee.cogs."+ext) 45 | eevee.load_extension(ext_name) 46 | 47 | if eevee.token is None or not eevee.default_prefix: 48 | eevee.logger.critical("Token and prefix must be set in order to login.") 49 | sys.exit(1) 50 | try: 51 | eevee.loop.run_until_complete(eevee.start(eevee.token)) 52 | except discord.LoginFailure: 53 | eevee.logger.critical("Invalid token") 54 | eevee.loop.run_until_complete(eevee.logout()) 55 | eevee.shutdown_mode = ExitCodes.SHUTDOWN 56 | except KeyboardInterrupt: 57 | eevee.logger.info("Keyboard interrupt detected. Quitting...") 58 | eevee.loop.run_until_complete(eevee.logout()) 59 | eevee.shutdown_mode = ExitCodes.SHUTDOWN 60 | except Exception as exc: 61 | eevee.logger.critical("Fatal exception", exc_info=exc) 62 | eevee.loop.run_until_complete(eevee.logout()) 63 | finally: 64 | code = eevee.shutdown_mode 65 | sys.exit(code.value) 66 | 67 | def parse_cli_args(): 68 | parser = argparse.ArgumentParser( 69 | description="Eevee - Pokemon Go Bot for Discord") 70 | parser.add_argument( 71 | "--debug", "-d", help="Enabled debug mode.", action="store_true") 72 | parser.add_argument( 73 | "--launcher", "-l", help=argparse.SUPPRESS, action="store_true") 74 | parser.add_argument( 75 | "--fromrestart", help=argparse.SUPPRESS, action="store_true") 76 | return parser.parse_args() 77 | 78 | 79 | 80 | def main(): 81 | args = parse_cli_args() 82 | run_eevee(debug=args.debug, launcher=args.launcher, from_restart=args.fromrestart) 83 | 84 | if __name__ == '__main__': 85 | main() 86 | -------------------------------------------------------------------------------- /eevee/cogs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scragly/Eevee/b71ec4ac74dc29a6774cc377fbde3d02d1e5a5c0/eevee/cogs/__init__.py -------------------------------------------------------------------------------- /eevee/cogs/community/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scragly/Eevee/b71ec4ac74dc29a6774cc377fbde3d02d1e5a5c0/eevee/cogs/community/__init__.py -------------------------------------------------------------------------------- /eevee/cogs/community/cog.py: -------------------------------------------------------------------------------- 1 | # add tag box -------------------------------------------------------------------------------- /eevee/cogs/dev/__init__.py: -------------------------------------------------------------------------------- 1 | """This cog contains primarily developer-focused features. 2 | 3 | There is a local check that limits all commands to be used by co-owners 4 | of the bot and above. 5 | """ 6 | 7 | from .cog import Dev 8 | from .unknown_events import UnknownEventLogging 9 | 10 | def setup(bot): 11 | bot.add_cog(Dev(bot)) 12 | bot.add_cog(UnknownEventLogging(bot)) 13 | -------------------------------------------------------------------------------- /eevee/cogs/dev/unknown_events.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | class UnknownEventLogging: 4 | def __init__(self, bot): 5 | self.bot = bot 6 | 7 | async def on_socket_response(self, msg): 8 | event = msg.get('t') 9 | if not event: 10 | return 11 | 12 | if hasattr(self.bot._connection, f'parse_{event.lower()}'): 13 | return 14 | 15 | log = logging.getLogger('discord') 16 | log.info(f'Unknown Discord Event {event}. Message: {msg}') 17 | print(f'Unknown Discord Event {event}') 18 | -------------------------------------------------------------------------------- /eevee/cogs/google/__init__.py: -------------------------------------------------------------------------------- 1 | """This cog contains google related features.""" 2 | 3 | from .google_cog import Google 4 | 5 | 6 | def setup(bot): 7 | bot.add_cog(Google(bot)) 8 | -------------------------------------------------------------------------------- /eevee/cogs/google/google_cog.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.parse import quote 3 | 4 | import bs4 5 | 6 | from discord.ext.commands import BadArgument 7 | 8 | from eevee import Cog, group, checks 9 | from eevee.utils import make_embed 10 | 11 | USER_AGENT = ("Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) " 12 | "Gecko/20100101 Firefox/50.0") 13 | 14 | 15 | class ImageResult: 16 | def __init__(self, data, session): 17 | if isinstance(data, str): 18 | data = json.loads(data) 19 | self._session = session 20 | self.url = data['ou'] 21 | self.title = data['s'] 22 | self.origin_url = data['ru'] 23 | self.thumb_url = data['tu'] 24 | 25 | @property 26 | def embed(self): 27 | return make_embed( 28 | title=self.title, 29 | title_url=self.origin_url, 30 | image=self.url, 31 | icon="https://image.flaticon.com/teams/slug/google.jpg") 32 | 33 | 34 | class ImageQuery: 35 | 36 | URL_FORMAT = "https://www.google.com/search?tbm=isch&q={}" 37 | 38 | def __init__(self, query, session): 39 | self.query = query 40 | self.results = None 41 | self.current_idx = 0 42 | self._session = session 43 | 44 | def parse(self, result): 45 | strainer = bs4.SoupStrainer('div', {'class': 'rg_meta notranslate'}) 46 | soup = bs4.BeautifulSoup(result, 'lxml', parse_only=strainer) 47 | results = soup.find_all('div', {'class': 'rg_meta notranslate'}) 48 | results = [ImageResult(r.string, self._session) for r in results] 49 | return results 50 | 51 | async def fetch_results(self): 52 | url = self.URL_FORMAT.format(quote(self.query)) 53 | headers = {'User-Agent': USER_AGENT} 54 | async with self._session.get(url, headers=headers) as r: 55 | html = await r.text() 56 | self.results = self.parse(html) 57 | return self.results 58 | 59 | @property 60 | def current_result(self): 61 | if not self.results: 62 | return None 63 | return self.results[self.current_idx] 64 | 65 | @property 66 | def embed(self): 67 | if not self.results: 68 | return None 69 | return self.current_result.embed 70 | 71 | @classmethod 72 | async def convert(cls, ctx, argument): 73 | instance = cls(argument, ctx.bot.session) 74 | results = await instance.fetch_results() 75 | if not results: 76 | raise BadArgument("No Results Found") 77 | return instance 78 | 79 | 80 | class WebResult: 81 | def __init__(self, data): 82 | title = data.find('h3', {'class': 'r'}) 83 | self.title = title.text 84 | self.title_url = title.find('a')['href'] 85 | self.info = data.find('span', {'class': 'st'}).text 86 | 87 | @property 88 | def embed(self): 89 | return make_embed(title=self.title, title_url=self.title_url, 90 | content=self.info) 91 | 92 | @property 93 | def title_only(self): 94 | return f"[{self.title}]({self.title_url})" 95 | 96 | @property 97 | def details(self): 98 | return f"**[{self.title}]({self.title_url})**\n{self.info}" 99 | 100 | 101 | class WebQuery: 102 | 103 | URL_FORMAT = "https://www.google.com/search?q={}" 104 | 105 | def __init__(self, query, session): 106 | self.query = query 107 | self.results = None 108 | self.current_idx = 0 109 | self._session = session 110 | 111 | def parse(self, result): 112 | strainer = bs4.SoupStrainer('div', {'class': 'rc'}) 113 | soup = bs4.BeautifulSoup(result, 'lxml', parse_only=strainer) 114 | results = soup.find_all('div', {'class': 'rc'}) 115 | results = [WebResult(r) for r in results] 116 | return results 117 | 118 | async def fetch_results(self): 119 | url = self.URL_FORMAT.format(quote(self.query)) 120 | headers = {'User-Agent': USER_AGENT} 121 | async with self._session.get(url, headers=headers) as r: 122 | html = await r.text() 123 | self.results = self.parse(html) 124 | return self.results 125 | 126 | @property 127 | def current_result(self): 128 | if not self.results: 129 | return None 130 | return self.results[self.current_idx] 131 | 132 | def embed(self, entries=6): 133 | if not self.results: 134 | return None 135 | 136 | top = [r.details for r in self.results[:3]] 137 | rest = ["\n\n**Other Results:**",] 138 | rest.extend([r.title_only for r in self.results[3:entries]]) 139 | 140 | return make_embed( 141 | title=f'Google Search for "{self.query}"', 142 | title_url=self.URL_FORMAT.format(quote(self.query)), 143 | content='\n\n'.join(top) + '\n'.join(rest), 144 | icon="https://image.flaticon.com/teams/slug/google.jpg") 145 | 146 | @classmethod 147 | async def convert(cls, ctx, argument): 148 | instance = cls(argument, ctx.bot.session) 149 | results = await instance.fetch_results() 150 | if not results: 151 | raise BadArgument("No Results Found") 152 | return instance 153 | 154 | 155 | class Google(Cog): 156 | def __init__(self, bot): 157 | self.bot = bot 158 | self.session = self.bot.session 159 | 160 | async def __local_check(self, ctx): 161 | return await checks.check_is_co_owner(ctx) 162 | 163 | @group(aliases=['g'], invoke_without_command=True) 164 | async def google(self, ctx, *, query: WebQuery): 165 | await ctx.embed(embed=query.embed()) 166 | 167 | @google.command(aliases=['image', 'images']) 168 | async def img(self, ctx, *, query: ImageQuery): 169 | await ctx.embed(embed=query.embed) 170 | -------------------------------------------------------------------------------- /eevee/cogs/pokemon/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import Pokedex 2 | from .objects import Pokemon 3 | 4 | def setup(bot): 5 | bot.add_cog(Pokedex(bot)) 6 | -------------------------------------------------------------------------------- /eevee/cogs/pokemon/cog.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | import sqlite3 5 | from functools import partial 6 | 7 | import discord 8 | 9 | from eevee import group 10 | from eevee.utils import make_embed, url_color 11 | from eevee.utils.converters import Multi 12 | from eevee.utils.formatters import code 13 | 14 | from .objects import Pokemon 15 | from .errors import PokemonNotFound 16 | 17 | def init_pokedata(bot): 18 | bot.pokemon = partial(Pokemon, bot) 19 | with open(os.path.join(bot.data_dir, "raid_info.json")) as fp: 20 | bot.raid_info_json = json.load(fp) 21 | bot.raid_pokemon = bot.raid_info_json["raid_pkmn"] 22 | bot.raid_eggs = bot.raid_info_json["raid_eggs"] 23 | with open(os.path.join(bot.data_dir, "pkmn_info.json")) as fp: 24 | bot.pkmn_info_json = json.load(fp) 25 | bot.pkmn_info = bot.pkmn_info_json["pokemon"] 26 | bot.type_chart = bot.pkmn_info_json["type_chart"] 27 | 28 | 29 | class Pokedex: 30 | """Pokemon Information and Management""" 31 | 32 | def __init__(self, bot): 33 | self.bot = bot 34 | self.bot.config.command_categories["Pokedex"] = { 35 | "index" : "30", 36 | "description" : "Pokemon Information and Management" 37 | } 38 | self.type_emoji = bot.config.type_emoji 39 | init_pokedata(bot) 40 | 41 | async def on_command_error(self, ctx, error): 42 | if isinstance(error, PokemonNotFound): 43 | await ctx.send(error.pokemon + ' not found.') 44 | 45 | def get_type_emoji(self, type): 46 | return self.type_emoji[type.lower()] 47 | 48 | def get_flavor(self, pkmn_id): 49 | conn = sqlite3.connect(os.path.join(self.bot.data_dir, 'pokedex-temp.sqlite')) 50 | c = conn.cursor() 51 | c.execute('SELECT species_id, flavor_text ' 52 | 'FROM pokemon_species_flavor_text ' 53 | 'WHERE version_id = 26 AND language_id = 9 ' 54 | 'AND species_id = {}'.format(pkmn_id)) 55 | return c.fetchone()[1] 56 | 57 | def pd_raid_info(self, pokemon): 58 | msg = ( 59 | f"`{'Level':<23}:` {pokemon.raid_level}\n" 60 | f"`{'Max Capture CP':<23}:` {pokemon.max_raid_cp()}\n" 61 | f"`{'Max CP w/ Weather Boost':<23}:` " 62 | f"{pokemon.max_raid_cp(weather_boost=True)}\n") 63 | if pokemon.is_exraid: 64 | msg += f"`{'Exraid':<23}:` {pokemon.is_exraid}" 65 | return msg 66 | 67 | def pd_type_info(self, pokemon): 68 | type_effects = pokemon.type_effects_grouped 69 | ue_emoji = ' '.join( 70 | self.get_type_emoji(t) for t in type_effects['ultra']) 71 | se_emoji = ' '.join( 72 | self.get_type_emoji(t) for t in type_effects['super']) 73 | le_emoji = ' '.join( 74 | self.get_type_emoji(t) for t in type_effects['low']) 75 | we_emoji = ' '.join( 76 | self.get_type_emoji(t) for t in type_effects['worst']) 77 | 78 | msg = "" 79 | if ue_emoji: 80 | msg += f'`Ultra Effective:` {ue_emoji}\n' 81 | if se_emoji: 82 | msg += f'`Super Effective:` {se_emoji}\n' 83 | if le_emoji: 84 | msg += f'`Low Effect :` {le_emoji}\n' 85 | if we_emoji: 86 | msg += f'`Worst Effect :` {we_emoji}\n' 87 | 88 | return msg 89 | 90 | async def pd_pokemon(self, pokemon, only_type=False, only_raid=False): 91 | pkmn_no = str(pokemon.id).zfill(3) 92 | pkmn_url = ('https://raw.githubusercontent.com/FoglyOgly/' 93 | f'Meowth/master/images/pkmn/{pkmn_no}_.png') 94 | pkmn_colour = await url_color(pkmn_url) 95 | embed = make_embed( 96 | image=pkmn_url, 97 | msg_colour=pkmn_colour) 98 | 99 | types_str = ' '.join(self.get_type_emoji(t) for t in pokemon.types) 100 | 101 | if only_type: 102 | header = (f'{types_str}\n#{pkmn_no} - {pokemon.name.capitalize()}' 103 | ' - Type Effectiveness') 104 | description = self.pd_type_info(pokemon) 105 | elif only_raid: 106 | header = (f'{types_str}\n#{pkmn_no} - {pokemon.name.capitalize()}' 107 | ' - Raid Info') 108 | if pokemon.is_raid: 109 | description = self.pd_raid_info(pokemon) 110 | else: 111 | description = "This Pokemon does not currently appear in raids." 112 | 113 | else: 114 | header = f'{types_str} #{pkmn_no} - {pokemon.name.capitalize()}' 115 | description = code(self.get_flavor(pokemon.id).replace('\n', ' ')) 116 | 117 | embed.add_field(name=header, value=description, inline=False) 118 | 119 | if only_type or only_raid: 120 | return embed 121 | 122 | embed.add_field(name='TYPE EFFECTIVENESS', 123 | value=self.pd_type_info(pokemon), 124 | inline=False) 125 | 126 | if pokemon.is_raid: 127 | embed.add_field(name='RAID INFO', 128 | value=self.pd_raid_info(pokemon), 129 | inline=False) 130 | 131 | return embed 132 | 133 | async def did_you_mean(self, ctx, pokemon): 134 | suggested = pokemon['suggested'] 135 | original = pokemon['original'] 136 | react_list = [ 137 | '\N{WHITE HEAVY CHECK MARK}', 138 | '\N{NEGATIVE SQUARED CROSS MARK}' 139 | ] 140 | dym_msg = await ctx.send(f'Did you mean "{suggested}"?') 141 | def check(reaction, user): 142 | if reaction.message.id != dym_msg.id: 143 | return False 144 | if user.id != ctx.author.id: 145 | return False 146 | if reaction.emoji not in react_list: 147 | return False 148 | return True 149 | for r in react_list: 150 | await dym_msg.add_reaction(r) 151 | try: 152 | reaction, user = await ctx.bot.wait_for('reaction_add', timeout=30, check=check) 153 | except asyncio.TimeoutError: 154 | try: 155 | await dym_msg.clear_reactions() 156 | except discord.Forbidden: 157 | pass 158 | return 159 | 160 | try: 161 | await dym_msg.clear_reactions() 162 | except discord.Forbidden: 163 | pass 164 | if reaction.emoji == '\N{WHITE HEAVY CHECK MARK}': 165 | ctx.message.content = ctx.message.content.replace( 166 | original, suggested) 167 | await dym_msg.delete() 168 | await ctx.bot.process_commands(ctx.message) 169 | elif reaction.emoji == '\N{NEGATIVE SQUARED CROSS MARK}': 170 | await dym_msg.delete() 171 | 172 | @group(category="Pokedex", aliases=["pd"], invoke_without_command=True) 173 | async def pokedex(self, ctx, *, pokemon: Pokemon): 174 | """Return Pokemon Info""" 175 | if isinstance(pokemon, Pokemon): 176 | embed = await self.pd_pokemon(pokemon) 177 | await ctx.send(embed=embed) 178 | elif isinstance(pokemon, dict): 179 | await self.did_you_mean(ctx, pokemon) 180 | else: 181 | await ctx.send(pokemon or "None") 182 | 183 | @pokedex.command() 184 | async def raid(self, ctx, *, arg: Multi(int, Pokemon)): 185 | """Return Raid Info""" 186 | if isinstance(arg, int): 187 | raid_level = arg 188 | raid_egg_url = ctx.bot.raid_eggs[f'{raid_level}']['img_url'] 189 | raid_egg_colour = await url_color(raid_egg_url) 190 | pkmn_list = [] 191 | for k, v in ctx.bot.raid_pokemon.items(): 192 | level = v['level'] 193 | if level == raid_level: 194 | pkmn_list.append(k) 195 | embed = make_embed( 196 | msg_type='info', 197 | title=f'Level {raid_level} Raid List', 198 | msg_colour=raid_egg_colour, content=', '.join(pkmn_list)) 199 | await ctx.send(embed=embed) 200 | 201 | elif isinstance(arg, Pokemon): 202 | embed = await self.pd_pokemon(arg, only_raid=True) 203 | await ctx.send(embed=embed) 204 | 205 | elif isinstance(arg, str): 206 | await self.did_you_mean(ctx, arg) 207 | 208 | 209 | # @pokedex.command() 210 | # async def dump(self, ctx): 211 | # conn = sqlite3.connect('F:/Github/veekun-pokedex.sqlite/veekun-pokedex.sqlite') 212 | # c = conn.cursor() 213 | # index = 1 214 | 215 | # for pkmn, data in ctx.bot.pkmn_info_json["pokemon"].items(): 216 | # c.execute('SELECT species_id, flavor_text ' 217 | # 'FROM pokemon_species_flavor_text ' 218 | # 'WHERE version_id = 26 AND language_id = 9 ' 219 | # 'AND species_id = {}'.format(index)) 220 | # flavor_data = c.fetchone() 221 | # if flavor_data: 222 | # flavor = flavor_data[1] 223 | # else: 224 | # flavor = None 225 | # new_dict['pokemon'][f'{index:03}'] = { 226 | # 'name_en': pkmn, 227 | # 'types' : data['types'] 228 | # } 229 | # if flavor: 230 | # new_dict['pokemon'][f'{index:03}']['flavor'] = flavor 231 | # index += 1 232 | # continue 233 | # with open(os.path.join(ctx.bot.data_dir, "raid_info_new.json"), mode='w') as fp: 234 | # json.dump(new_dict, fp, indent=4, sort_keys=True) 235 | # await ctx.send('complete') 236 | -------------------------------------------------------------------------------- /eevee/cogs/pokemon/errors.py: -------------------------------------------------------------------------------- 1 | from discord.ext.commands import CommandError 2 | 3 | class PokemonNotFound(CommandError): 4 | """Exception raised when Pokemon given does not exist.""" 5 | def __init__(self, pokemon, retry=True): 6 | self.pokemon = pokemon 7 | self.retry = retry 8 | -------------------------------------------------------------------------------- /eevee/cogs/pokemon/objects.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | 3 | from eevee.utils import fuzzymatch, url_color 4 | 5 | from .errors import PokemonNotFound 6 | 7 | 8 | class Pokemon(): 9 | """Represents a Pokemon. 10 | 11 | This class contains the attributes of a specific pokemon, and 12 | provides methods of which to get specific info and results on it. 13 | 14 | Parameters 15 | ----------- 16 | bot: :class:`eevee.core.bot.Eevee` 17 | Current instance of Eevee 18 | pkmn: str or int 19 | The name or id of a Pokemon 20 | guild: :class:`discord.Guild`, optional 21 | The guild that is requesting the Pokemon 22 | moveset: :class:`list` or :class:`tuple` of :class:`str`, optional 23 | `kwarg-only:` The two moves of this Pokemon 24 | weather: :class:`str`, optional 25 | `kwarg-only:` Weather during the encounter 26 | 27 | Raises 28 | ------- 29 | :exc:`.errors.PokemonNotFound` 30 | The pkmn argument was not a valid index and was not found in the 31 | list of Pokemon names. 32 | 33 | Attributes 34 | ----------- 35 | name: :class:`str` 36 | Lowercase string representing the name of the Pokemon 37 | id: :class:`int` 38 | Pokemon ID number 39 | types: :class:`list` of :class:`str` 40 | A :class:`list` of the Pokemon's types 41 | moveset: :class:`list` or :class:`tuple` of :class:`str` 42 | The two moves of this Pokemon 43 | weather: :class:`str` 44 | Weather during the encounter 45 | guild: :class:`discord.Guild` 46 | Guild that created the Pokemon 47 | bot: :class:`eevee.core.bot.Eevee` 48 | Current instance of Eevee 49 | """ 50 | 51 | __slots__ = ('name', 'id', 'types', 'bot', 'guild', 'pkmn_list', 52 | 'pb_raid', 'weather', 'moveset') 53 | 54 | def __init__(self, bot, pkmn, guild=None, **attribs): 55 | self.bot = bot 56 | self.guild = guild 57 | self.pkmn_list = list(bot.pkmn_info.keys()) 58 | if pkmn.isdigit(): 59 | try: 60 | pkmn = self.pkmn_list[int(pkmn)-1] 61 | except IndexError: 62 | pass 63 | self.name = pkmn 64 | if pkmn not in self.pkmn_list: 65 | raise PokemonNotFound(pkmn) 66 | self.id = self.pkmn_list.index(pkmn)+1 67 | self.types = self._get_type(self.name) 68 | self.pb_raid = None 69 | self.weather = attribs.get('weather', None) 70 | self.moveset = attribs.get('moveset', []) 71 | 72 | def __str__(self): 73 | return self.name.title() 74 | 75 | async def get_pb_raid(self, weather=None, userid=None, moveset=None): 76 | """Get a PokeBattler Raid for this Pokemon 77 | 78 | This can quickly produce a PokeBattler Raid for the current 79 | Pokemon, with the option of providing a PokeBattler User ID to 80 | get customised results. 81 | 82 | The resulting PokeBattler Raid object will be saved under the 83 | `pb_raid` attribute of the Pokemon instance for later retrieval, 84 | unless it's customised with an ID. 85 | 86 | Parameters 87 | ----------- 88 | weather: :class:`str`, optional 89 | The weather during the raid 90 | userid: :class:`int`, optional 91 | The Pokebattler User ID to generate the PB Raid with 92 | moveset: list or tuple, optional 93 | A :class:`list` or :class:`tuple` with a :class:`str` representing 94 | ``move1`` and ``move2`` of the Pokemon. 95 | 96 | Returns 97 | -------- 98 | :class:`eevee.cogs.pokebattler.objects.PBRaid` or :obj:`None` 99 | PokeBattler Raid instance or None if not a Raid Pokemon. 100 | 101 | Example 102 | -------- 103 | 104 | .. code-block:: python3 105 | 106 | pokemon = Pokemon(ctx.bot, 'Groudon') 107 | moveset = ('Dragon Tail', 'Solar Beam') 108 | pb_raid = pokemon.get_pb_raid('windy', 12345, moveset) 109 | """ 110 | 111 | # if a Pokebattler Raid exists with the same settings, return it 112 | if self.pb_raid: 113 | if not (weather or userid) and not moveset: 114 | return self.pb_raid 115 | if weather: 116 | self.pb_raid.change_weather(weather) 117 | 118 | # if it doesn't exist or settings changed, generate it 119 | else: 120 | pb_cog = self.bot.cogs.get('PokeBattler', None) 121 | if not pb_cog: 122 | return None 123 | if not weather: 124 | weather = self.weather or 'DEFAULT' 125 | weather = pb_cog.PBRaid.get_weather(weather) 126 | pb_raid = await pb_cog.PBRaid.get( 127 | self.bot, self, weather=self.weather, userid=userid) 128 | 129 | # set the moveset for the Pokebattler Raid 130 | if not moveset: 131 | moveset = self.moveset 132 | try: 133 | pb_raid.set_moveset(moveset) 134 | except RuntimeError: 135 | pass 136 | 137 | # don't save it if it's a user-specific Pokebattler Raid 138 | if not userid: 139 | self.pb_raid = pb_raid 140 | 141 | return pb_raid 142 | 143 | @property 144 | def img_url(self): 145 | """:class:`str` : Pokemon sprite image URL""" 146 | pkmn_no = str(self.id).zfill(3) 147 | return ('https://raw.githubusercontent.com/FoglyOgly/' 148 | f'Meowth/master/images/pkmn/{pkmn_no}_.png') 149 | 150 | async def colour(self): 151 | """:class:`discord.Colour` : Discord colour based on Pokemon sprite.""" 152 | return await url_color(self.img_url) 153 | 154 | @property 155 | def is_raid(self): 156 | """:class:`bool` : Indicates if the pokemon can show in Raids""" 157 | return self.name in list(self.bot.raid_pokemon.keys()) 158 | 159 | @property 160 | def is_exraid(self): 161 | """:class:`bool` : Indicates if the pokemon can show in Raids""" 162 | if not self.is_raid: 163 | return False 164 | return self.bot.raid_pokemon[self.name].get('exraid', False) 165 | 166 | @property 167 | def raid_level(self): 168 | """:class:`int` or :obj:`None` : Returns raid egg level""" 169 | return (self.bot.raid_pokemon[self.name]["level"] 170 | if self.is_raid else None) 171 | 172 | def max_raid_cp(self, weather_boost=False): 173 | """:class:`int` or :obj:`None` : Returns max CP on capture after raid 174 | """ 175 | key = "max_cp_w" if weather_boost else "max_cp" 176 | return self.bot.raid_pokemon[self.name][key] if self.is_raid else None 177 | 178 | def role(self, guild=None): 179 | """:class:`discord.Role` or :obj:`None` : Returns the role for 180 | this Pokemon 181 | """ 182 | if not guild: 183 | guild = self.guild 184 | if not guild: 185 | return None 186 | return self.bot.get(guild.roles, name=self.name) 187 | 188 | def set_guild(self, guild): 189 | """:class:`discord.Guild` or :obj:`None` : Sets the relevant Guild""" 190 | self.guild = guild 191 | 192 | @property 193 | def weak_against(self): 194 | """:class:`dict` : Returns a dict of all types the Pokemon is 195 | weak against. 196 | """ 197 | types_eff = {} 198 | for t, v in self.type_effects.items(): 199 | if round(v, 3) > 1: 200 | types_eff[t] = v 201 | return types_eff 202 | 203 | @property 204 | def strong_against(self): 205 | """:class:`dict` : Returns a dict of all types the Pokemon is 206 | strong against. 207 | """ 208 | types_eff = {} 209 | for t, v in self.type_effects.items(): 210 | if round(v, 3) < 1: 211 | types_eff[t] = v 212 | return types_eff 213 | 214 | def _get_type(self, pkmn): 215 | """:class:`list` : Returns the Pokemon's types""" 216 | return self.bot.pkmn_info[pkmn]["types"] 217 | 218 | @property 219 | def type_effects(self): 220 | """:class:`dict` : Returns a dict of all Pokemon types and their 221 | relative effectiveness as values. 222 | """ 223 | type_eff = {} 224 | for _type in self.types: 225 | for atk_type in self.bot.type_chart[_type]: 226 | if atk_type not in type_eff: 227 | type_eff[atk_type] = 1 228 | type_eff[atk_type] *= self.bot.type_chart[_type][atk_type] 229 | return type_eff 230 | 231 | @property 232 | def type_effects_grouped(self): 233 | """:class:`dict` : Returns a dict of all Pokemon types and their 234 | relative effectiveness as values, grouped by the following: 235 | * ultra 236 | * super 237 | * low 238 | * worst 239 | """ 240 | type_eff_dict = { 241 | 'ultra' : [], 242 | 'super' : [], 243 | 'low' : [], 244 | 'worst' : [] 245 | } 246 | for t, v in self.type_effects.items(): 247 | if v > 1.9: 248 | type_eff_dict['ultra'].append(t) 249 | elif v > 1.3: 250 | type_eff_dict['super'].append(t) 251 | elif v < 0.6: 252 | type_eff_dict['worst'].append(t) 253 | else: 254 | type_eff_dict['low'].append(t) 255 | return type_eff_dict 256 | 257 | @classmethod 258 | async def convert(cls, ctx, argument): 259 | """Returns a pokemon that matches the value 260 | of the argument that's being converted. 261 | 262 | It first will check if it's a valid ID, and if not, will perform 263 | a fuzzymatch against the list of Pokemon names. 264 | 265 | Returns 266 | -------- 267 | :class:`Pokemon` or :class:`dict` 268 | If there was a close or exact match, it will return a valid 269 | :class:`Pokemon`. 270 | If the match is lower than 80% likeness, it will return a 271 | :class:`dict` with the following keys: 272 | * ``suggested`` - Next best guess based on likeness. 273 | * ``original`` - Original value of argument provided. 274 | 275 | Raises 276 | ------- 277 | :exc:`discord.ext.commands.BadArgument` 278 | The argument didn't match a Pokemon ID or name. 279 | """ 280 | if argument.isdigit(): 281 | try: 282 | match = list(ctx.bot.pkmn_info.keys())[int(argument)-1] 283 | score = 100 284 | except IndexError: 285 | raise commands.errors.BadArgument( 286 | 'Pokemon ID "{}" not valid'.format(argument)) 287 | else: 288 | pkmn_list = list(ctx.bot.pkmn_info.keys()) 289 | match, score = fuzzymatch.get_match(pkmn_list, argument) 290 | if match: 291 | if score >= 80: 292 | result = cls(ctx.bot, str(match), ctx.guild) 293 | else: 294 | result = { 295 | 'suggested' : str(match), 296 | 'original' : argument 297 | } 298 | 299 | if not result: 300 | raise commands.errors.BadArgument( 301 | 'Pokemon "{}" not valid'.format(argument)) 302 | 303 | return result 304 | 305 | class RaidEgg: 306 | 307 | @classmethod 308 | async def convert(cls, ctx, argument): 309 | return cls(argument) 310 | -------------------------------------------------------------------------------- /eevee/cogs/public_tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Public Tests Features Module""" 2 | 3 | from .cog import PublicTests 4 | 5 | def setup(bot): 6 | bot.add_cog(PublicTests(bot)) 7 | -------------------------------------------------------------------------------- /eevee/cogs/public_tests/cog.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | import datetime 4 | import json 5 | import re 6 | from io import BytesIO 7 | 8 | import async_timeout 9 | import discord 10 | from PIL import Image, ImageDraw, ImageOps 11 | 12 | from eevee import group, command, checks, utils 13 | 14 | 15 | class BaseConverter(object): 16 | decimal_digits = "0123456789" 17 | 18 | def __init__(self, digits): 19 | self.digits = digits 20 | 21 | def from_decimal(self, i): 22 | return self.convert(i, self.decimal_digits, self.digits) 23 | 24 | def to_decimal(self, s): 25 | return int(self.convert(s, self.digits, self.decimal_digits)) 26 | 27 | def convert(number, fromdigits, todigits): 28 | # Based on http://code.activestate.com/recipes/111286/ 29 | if str(number)[0] == '-': 30 | number = str(number)[1:] 31 | neg = 1 32 | else: 33 | neg = 0 34 | 35 | # make an integer out of the number 36 | x = 0 37 | for digit in str(number): 38 | x = x * len(fromdigits) + fromdigits.index(digit) 39 | 40 | # create the result in base 'len(todigits)' 41 | if x == 0: 42 | res = todigits[0] 43 | else: 44 | res = "" 45 | while x > 0: 46 | digit = x % len(todigits) 47 | res = todigits[digit] + res 48 | x = int(x / len(todigits)) 49 | if neg: 50 | res = '-' + res 51 | return res 52 | convert = staticmethod(convert) 53 | 54 | 55 | class PublicTests: 56 | """Test commands that are open for public usage.""" 57 | def __init__(self, bot): 58 | self.bot = bot 59 | 60 | async def get_prefixes(self, guild_id, bot_id=None): 61 | table = self.bot.dbi.tablenew('bot_prefixes') 62 | if bot_id: 63 | table.query('prefixes') 64 | table.query.where(guild_id=guild_id, bot_id=bot_id) 65 | try: 66 | return await table.query.get_value() 67 | except IndexError: 68 | return None 69 | else: 70 | table.query('prefixes', 'bot_id') 71 | table.query.where(guild_id=guild_id) 72 | try: 73 | return await table.query.get() 74 | except IndexError: 75 | return None 76 | 77 | async def set_prefixes(self, guild_id, bot_id, prefixes: list): 78 | await self.bot.dbi.upsert( 79 | 'bot_prefixes', primary=['bot_id', 'guild_id'], bot_id=bot_id, 80 | guild_id=guild_id, prefixes=prefixes) 81 | return prefixes 82 | 83 | async def add_prefixes(self, bot_id, guild_id, prefixes: list): 84 | existing = await self.get_prefixes(bot_id, guild_id) 85 | prefixes.extend(existing) 86 | await self.set_prefixes(bot_id, guild_id, prefixes) 87 | return prefixes 88 | 89 | async def rm_prefixes(self, bot_id, guild_id, prefixes: list): 90 | existing = await self.get_prefixes(bot_id, guild_id) 91 | prefixes.extend(existing) 92 | await self.set_prefixes(bot_id, guild_id, prefixes) 93 | return prefixes 94 | 95 | async def del_bot(self, guild_id, bot_id): 96 | await self.bot.dbi.delete('bot_prefixes', 97 | bot_id=bot_id, 98 | guild_id=guild_id) 99 | 100 | @group(invoke_without_command=True) 101 | async def botprefixes(self, ctx, *, bot: discord.Member = None): 102 | if not bot: 103 | prefixes = [] 104 | results = await self.get_prefixes(ctx.guild.id) 105 | for botdata in results: 106 | bot = ctx.get.member(botdata['bot_id']) 107 | prefixlist = [f'`"{p}"`' for p in botdata['prefixes']] 108 | prefixes.append(f"{bot.mention}\n{', '.join(prefixlist)}") 109 | await ctx.embed('Bot Prefixes', '\n'.join(prefixes)) 110 | else: 111 | if not bot.bot: 112 | return await ctx.error("That's not a bot.") 113 | prefixes = await self.get_prefixes(ctx.guild.id, bot.id) 114 | await ctx.send(str(prefixes)) 115 | 116 | @botprefixes.command(name='set') 117 | async def _set(self, ctx, bot: discord.Member, *prefixes): 118 | if not bot.bot: 119 | return await ctx.error("That's not a bot.") 120 | await self.set_prefixes(ctx.guild.id, bot.id, list(prefixes)) 121 | return await ctx.success( 122 | f"Registered prefixes {', '.join(prefixes)} for {bot.display_name}") 123 | 124 | @botprefixes.command(name='add') 125 | async def _add(self, ctx, bot: discord.Member, *prefixes): 126 | if not bot.bot: 127 | return await ctx.error("That's not a bot.") 128 | all_prefixes = await self.add_prefixes(ctx.guild.id, bot.id, list(prefixes)) 129 | return await ctx.success( 130 | f"Registered new prefixes {', '.join(prefixes)} for {bot.display_name}", 131 | f"Full list:\n{', '.join(all_prefixes)}") 132 | 133 | @botprefixes.command(name='rm', aliases=['remove', 'delete', 'clear']) 134 | async def _rm(self, ctx, *, bot: discord.Member): 135 | await self.del_bot(ctx.guild.id, bot.id) 136 | return await ctx.success(f"Removed bot {bot} from register.") 137 | 138 | @command() 139 | async def fuzz_test(self, ctx, word, *word_list): 140 | match, score = utils.get_match(word_list, word) 141 | if not match: 142 | return await ctx.send("The word did not meet the minimum likeness threshold.") 143 | await ctx.send(f"The word {word} matched {match} with a score of {score}") 144 | 145 | @group(invoke_without_command=True) 146 | async def mystxd(self, ctx): 147 | """How many times has Myst XD""" 148 | table = ctx.bot.dbi.table('discord_messages') 149 | table.query(table['*'].count).where(author_id=402159684724719617) 150 | table.query.where(table['clean_content'].ilike('%xd%')) 151 | xds = await table.query.get_value() 152 | await ctx.embed(f"I've seen Myst XD {xds} times.") 153 | 154 | @mystxd.command(aliases=['hr']) 155 | async def past(self, ctx, minutes: int): 156 | """How many times has Myst XD in the past x mins""" 157 | after = time.time() - (minutes * 60) 158 | table = ctx.bot.dbi.table('discord_messages') 159 | table.query(table['*'].count).where(author_id=402159684724719617) 160 | table.query.where(table['clean_content'].ilike('%xd%')) 161 | table.query.where(table['sent'] > int(after)) 162 | xds = await table.query.get_value() 163 | await ctx.embed(f"I've seen Myst XD {xds} times in {minutes} mins.") 164 | 165 | @command() 166 | async def show_original(self, ctx, message_id: int): 167 | msg_table = ctx.bot.dbi.table('discord_messages') 168 | msg_table.query( 169 | 'message_id', 'sent', 'is_edit', 'author_id', 'clean_content', 170 | 'embeds', 'attachments') 171 | msg_table.query.where(message_id=message_id) 172 | msg_table.query.where(is_edit=False) 173 | msg_table.query.order_by('message_id', 'sent', asc=False) 174 | msg_table.query.limit(1) 175 | 176 | msg = await msg_table.query.get_first() 177 | 178 | print(msg) 179 | 180 | if not msg: 181 | return await ctx.embed( 182 | "I didn't find any deleted messages, sorry!") 183 | 184 | author = await ctx.get.user(msg['author_id']) 185 | date = datetime.datetime.fromtimestamp(int(msg['sent'])) 186 | date_str = date.strftime('%Y-%m-%d %H:%M:%S') 187 | content = msg['clean_content'] 188 | embeds = msg['embeds'] 189 | if len(embeds) > 1: 190 | content = f'{len(embeds)} Embeds' 191 | elif embeds: 192 | embed = json.loads(embeds[0]) 193 | embed_content = [] 194 | if embed.get('author'): 195 | embed_content.append( 196 | f"AuthorTitle: {embed['author']['name']}") 197 | if embed.get('title'): 198 | embed_content.append( 199 | f"Title: {embed['title']}") 200 | if embed.get('description'): 201 | embed_content.append( 202 | f"Description: {embed['description']}") 203 | if embed.get('fields'): 204 | embed_content.append( 205 | f"Field Count: {len(embed['fields'])}") 206 | if embed.get('color'): 207 | embed_content.append( 208 | f"Colour: {embed['color']}") 209 | if embed_content: 210 | embed_content = '\n'.join(embed_content) 211 | else: 212 | embed_content = '1 Embed' 213 | 214 | content += f"\n**Embeds:**\n{embed_content}" 215 | 216 | await ctx.embed(f"{author} | ID {message_id}", content) 217 | 218 | @command() 219 | async def base_converter(self, ctx, digits, *values): 220 | """Converts any number base with any representation of digits given.""" 221 | base_convert = BaseConverter(digits) 222 | converted_values = map(base_convert.to_decimal, values) 223 | converted_values = map(str, converted_values) 224 | await ctx.send('\n'.join(converted_values)) 225 | 226 | @command() 227 | async def quote(self, ctx, message_id: int): 228 | if message_id < 20000000000000000: 229 | return await ctx.error( 230 | "That doesn't seem to be a valid message ID!") 231 | table = ctx.bot.dbi.table('discord_messages') 232 | table.query( 233 | 'clean_content', 'author_id', 'sent', 'channel_id', 'guild_id') 234 | table.query.where(message_id=message_id) 235 | table.query.order_by('sent', asc=False) 236 | message_data = await table.query.get_first() 237 | if not message_data: 238 | return await ctx.error( 239 | "I can't see a message with that ID, sorry!") 240 | sent_by = await ctx.get.user(message_data['author_id']) 241 | sent = datetime.datetime.fromtimestamp(message_data['sent']) 242 | sent_str = sent.strftime('%Y-%m-%d %H:%M:%S') 243 | channel = ctx.bot.get_channel(message_data['channel_id']) 244 | guild = ctx.get.guild(message_data['guild_id']) 245 | await ctx.embed( 246 | f'Quote from {sent_by}', 247 | utils.formatters.code(message_data['clean_content']), 248 | footer=f"Sent at {sent_str} in {channel} - {guild}") 249 | 250 | @command(aliases=['priv', 'privs']) 251 | async def privilege(self, ctx, member: discord.Member = None): 252 | if member: 253 | if not await checks.check_is_mod(ctx): 254 | return await ctx.error( 255 | "Only mods can check other member's privilege level.") 256 | member = member or ctx.author 257 | ctx.author = member 258 | if not await checks.check_is_mod(ctx): 259 | return await ctx.info('Normal User') 260 | if not await checks.check_is_admin(ctx): 261 | return await ctx.info('Mod') 262 | if not await checks.check_is_guildowner(ctx): 263 | return await ctx.info('Admin') 264 | if not await checks.check_is_co_owner(ctx): 265 | return await ctx.info('Guild Owner') 266 | if not await checks.check_is_owner(ctx): 267 | return await ctx.info('Bot Co-Owner') 268 | return await ctx.info('Bot Owner') 269 | 270 | @command(aliases=["hello", "g'day", "gday", "whatsupcobba", "topofthemorning", "hola"]) 271 | async def hi(self, ctx): 272 | await ctx.embed(f"Hi {ctx.author.display_name} \U0001f44b") 273 | 274 | @group(name='embed', invoke_without_command=True) 275 | async def _embed(self, ctx, title=None, content=None, colour=None, 276 | icon_url=None, image=None, thumbnail=None, footer=None, 277 | footer_icon=None, plain_msg=''): 278 | await ctx.embed(title=title, description=content, colour=colour, 279 | icon=icon_url, image=image, thumbnail=thumbnail, 280 | plain_msg=plain_msg, footer=footer, 281 | footer_icon=footer_icon) 282 | try: 283 | await ctx.message.delete() 284 | except discord.Forbidden: 285 | pass 286 | 287 | @_embed.command(name='error') 288 | async def _error(self, ctx, title, content=None, log_level='warning'): 289 | await ctx.error(title, content, log_level) 290 | 291 | @_embed.command(name='info') 292 | async def _info(self, ctx, title, content=None): 293 | await ctx.info(title, content) 294 | 295 | @_embed.command(name='warning') 296 | async def _warning(self, ctx, title, content=None): 297 | embed = utils.make_embed(title=title, content=content, msg_type='warning') 298 | await ctx.send(embed=embed) 299 | 300 | @_embed.command(name='success') 301 | async def _success(self, ctx, title, content=None): 302 | embed = utils.make_embed(title=title, content=content, msg_type='success') 303 | await ctx.send(embed=embed) 304 | 305 | @_embed.command(name='help') 306 | async def _help(self, ctx, title, content=None): 307 | embed = utils.make_embed(title=title, content=content, msg_type='help') 308 | await ctx.send(embed=embed) 309 | 310 | @command(aliases=['avatar']) 311 | async def avy(self, ctx, member: discord.Member = None, size: utils.bitround = 1024): 312 | member = member or ctx.author 313 | avy_url = member.avatar_url_as(size=size, static_format='png') 314 | try: 315 | colour = await utils.user_color(member) 316 | except OSError: 317 | colour = ctx.me.colour 318 | await ctx.embed( 319 | f"{member.display_name}'s Avatar", title_url=avy_url, image=avy_url, colour=colour) 320 | 321 | @command() 322 | async def cleanup(self, ctx, after_msg_id: int, channel_id: int = None): 323 | after_msg = await ctx.get.message(after_msg_id) 324 | channel = ctx.channel 325 | if channel_id: 326 | channel = ctx.get.channel(channel_id) 327 | 328 | def is_eevee(msg): 329 | return msg.author == ctx.bot.user 330 | 331 | try: 332 | deleted = await channel.purge( 333 | after=after_msg, check=is_eevee, bulk=True) 334 | except discord.Forbidden: 335 | deleted = await channel.purge( 336 | after=after_msg, check=is_eevee, bulk=False) 337 | 338 | del_count = len(deleted) 339 | 340 | embed = utils.make_embed( 341 | msg_type='success', 342 | title=f"Deleted {del_count} message{'s' if del_count > 1 else ''}" 343 | ) 344 | 345 | result_msg = await ctx.send(embed=embed) 346 | await asyncio.sleep(3) 347 | await result_msg.delete() 348 | 349 | @command(aliases=['igpayatinlay']) 350 | async def piglatin(self, ctx, *words): 351 | if not words: 352 | return await ctx.send('Onay Ordsway Otay Onvertcay') 353 | result = [] 354 | for word in words: 355 | if not word: 356 | result.append('') 357 | continue 358 | word = word.lower() 359 | pattern = re.compile('[a,e,i,o,u]') 360 | y = 'y' 361 | tail = 'a' + y 362 | if word.startswith(y): 363 | result.append(word + y + tail) 364 | continue 365 | first_vowel = pattern.search(word) 366 | if not first_vowel: 367 | result.append(word + tail) 368 | continue 369 | first_vowel = first_vowel.group() 370 | if word.find(first_vowel) == 0: 371 | result.append(word + y + tail) 372 | continue 373 | first, second = word.split(first_vowel, 1) 374 | result.append(first_vowel + second + first + tail) 375 | await ctx.send(' '.join(result)) 376 | 377 | async def circle_crop(self, img_url): 378 | async with self.bot.session.get(img_url) as r: 379 | data = BytesIO(await r.read()) 380 | img = Image.open(data) 381 | size = img.size 382 | with Image.new('L', size, 255) as mask: 383 | draw = ImageDraw.Draw(mask) 384 | draw.ellipse((0, 0) + size, fill=0) 385 | del draw 386 | img = img.convert('RGBA') 387 | output = ImageOps.fit(img, mask.size, centering=(0.5, 0.5)) 388 | output.paste(0, mask=mask) 389 | b = BytesIO() 390 | output.save(b, 'png') 391 | b.seek(0) 392 | return b 393 | 394 | @command() 395 | async def profile_preview(self, ctx, url=None): 396 | img_urls = [] 397 | if not url: 398 | if ctx.message.attachments: 399 | for img in ctx.message.attachments: 400 | img_urls.append(img.url) 401 | else: 402 | img_urls.append(ctx.author.avatar_url_as(format='png')) 403 | else: 404 | img_urls.append(url) 405 | imgs = [] 406 | for img_url in img_urls: 407 | imgs.append(await self.circle_crop(img_url)) 408 | 409 | for img in imgs: 410 | await ctx.send(file=discord.File(img, filename='circle.png')) 411 | 412 | @command() 413 | async def vote(self, ctx, title, *, content): 414 | uv = ctx.get.emoji('upvote') 415 | dv = ctx.get.emoji('downvote') 416 | msg = await ctx.embed(title, content) 417 | await msg.add_reaction(uv) 418 | await asyncio.sleep(0.5) 419 | await msg.add_reaction(dv) 420 | await ctx.message.delete() 421 | -------------------------------------------------------------------------------- /eevee/cogs/stats/__init__.py: -------------------------------------------------------------------------------- 1 | """This is a statistics cog.""" 2 | 3 | from .cog import Statistics 4 | 5 | def setup(bot): 6 | bot.add_cog(Statistics(bot)) 7 | -------------------------------------------------------------------------------- /eevee/cogs/stats/cog.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import typing 4 | 5 | import discord 6 | import matplotlib 7 | 8 | if os.name != 'nt': 9 | matplotlib.use('Agg') 10 | 11 | import matplotlib.pyplot as plt 12 | import matplotlib.dates as mdates 13 | 14 | from eevee import checks, command 15 | from eevee.utils.converters import Guild 16 | 17 | 18 | class Statistics: 19 | """Statistics Tools""" 20 | def __init__(self, bot): 21 | self.bot = bot 22 | 23 | @command() 24 | async def msgcount(self, ctx, member: typing.Union[discord.Member, Guild] = None): 25 | guild = None 26 | if isinstance(member, discord.Guild): 27 | if await checks.check_is_owner(ctx): 28 | guild = member 29 | member = ctx.author 30 | else: 31 | member = None 32 | 33 | guild = guild or ctx.guild 34 | member = member or ctx.author 35 | query = ctx.bot.dbi.table('discord_messages').query('sent') 36 | query.where(guild_id=guild.id, is_edit=False, author_id=member.id) 37 | query.order_by('sent', asc=False) 38 | data = await query.get_values() 39 | 40 | if not data: 41 | return await ctx.error( 42 | f"I haven't seen {member.display_name} before.") 43 | 44 | dates = mdates.epoch2num(data) 45 | 46 | fig, ax = plt.subplots(linewidth=0, sharey=True, tight_layout=True) 47 | fig.set_size_inches(8, 4) 48 | ax.tick_params(labelsize=12, color='lightgrey', labelcolor='lightgrey') 49 | 50 | locator = mdates.AutoDateLocator() 51 | ax.xaxis.set_major_locator(locator) 52 | ax.xaxis.set_major_formatter(mdates.DateFormatter('%d %b')) 53 | 54 | counts, bins, patches = ax.hist(dates, 10, facecolor='red', alpha=0.75) 55 | ax.set_xticks(bins) 56 | 57 | plot_bytes = io.BytesIO() 58 | fig.savefig( 59 | plot_bytes, 60 | format='png', 61 | facecolor='#32363C', 62 | transparent=True) 63 | plot_bytes.seek(0) 64 | fig.clf() 65 | 66 | fname = f"msgcount-{member.id}.png" 67 | plot_file = discord.File(plot_bytes, filename=fname) 68 | 69 | embed = await ctx.embed( 70 | f"Message Stats - {member.display_name} in {guild.name}", send=False) 71 | embed.set_image(url=f"attachment://{fname}") 72 | await ctx.send(file=plot_file, embed=embed) 73 | 74 | @command() 75 | async def mostactive(self, ctx): 76 | table = ctx.bot.dbi.table('discord_messages') 77 | query = table.query( 78 | 'author_id', 79 | table['message_id'].count, 80 | "rank() over (order by count(message_id) desc) as rank") 81 | query.where(guild_id=ctx.guild.id, is_edit=False) 82 | query.order_by('count', asc=False) 83 | query.group_by('author_id') 84 | data = await query.get() 85 | 86 | if not data: 87 | return await ctx.error('No data found.') 88 | 89 | author_data = [m for m in data if m['author_id'] == ctx.author.id][0] 90 | 91 | data = { 92 | ( 93 | str(ctx.get.member(m['author_id'], ctx.guild.id)) 94 | if ctx.get.member(m['author_id'], ctx.guild.id) else str(m['author_id']) 95 | ) :m['count'] for m in data[:10] 96 | } 97 | 98 | if author_data and str(ctx.author) not in data: 99 | data["..."] = 0 100 | author_key = f"#{author_data['rank']} - {ctx.author}" 101 | data[author_key] = author_data['count'] 102 | 103 | # ax.bar(x, y, color='r', width=1.0, linewidth=0, tick_label=labels) 104 | 105 | matplotlib.rc('font', family='Roboto Medium') 106 | 107 | fig, ax = plt.subplots(linewidth=0, sharey=True, tight_layout=True) 108 | fig.set_size_inches(8, 5) 109 | ax.tick_params(labelsize=12, color='lightgrey', labelcolor='lightgrey') 110 | ax.barh(list(data.keys()), list(data.values()), color='r', height=1.0, linewidth=1, edgecolor='black') 111 | ax.invert_yaxis() 112 | 113 | plot_bytes = io.BytesIO() 114 | fig.savefig( 115 | plot_bytes, 116 | format='png', 117 | facecolor='#32363C', 118 | transparent=True, 119 | antialiased=True) 120 | plot_bytes.seek(0) 121 | fig.clf() 122 | 123 | fname = f"mostactive-{ctx.guild.id}.png" 124 | plot_file = discord.File(plot_bytes, filename=fname) 125 | 126 | embed = await ctx.embed( 127 | f"Message Activity Per Member - {ctx.guild.name}", send=False) 128 | embed.set_image(url=f"attachment://{fname}") 129 | await ctx.send(file=plot_file, embed=embed) 130 | 131 | @command() 132 | async def msgactivity(self, ctx): 133 | """ 134 | Shows the hourly message activity from saved message history 135 | for all channels the bot can see. 136 | """ 137 | 138 | table = ctx.bot.dbi.table('discord_messages') 139 | table.query('count(*) AS count', "date_part('hour', to_timestamp(sent)) AS hour") 140 | table.query.where(is_edit=False, guild_id=ctx.guild.id).group_by('hour') 141 | rawdata = await table.query.get() 142 | data = {r['hour']: r['count'] for r in rawdata} 143 | 144 | matplotlib.rc('font', family='Roboto Medium') 145 | fig, ax = plt.subplots(linewidth=0, tight_layout=True) 146 | fig.set_size_inches(12, 5) 147 | ax.tick_params(labelsize=20, color='lightgrey', labelcolor='lightgrey') 148 | ax.plot(list(data.keys()), list(data.values()), color='r', linewidth=4) 149 | ax.set_xlabel("UTC Hours", color='lightgrey', fontsize="xx-large") 150 | plt.xticks(list(data.keys())) 151 | for i in list(data.keys()): 152 | ax.axvline(x=i) 153 | 154 | plot_bytes = io.BytesIO() 155 | 156 | fig.savefig( 157 | plot_bytes, 158 | format='png', 159 | facecolor='#32363C', 160 | transparent=True, 161 | antialiased=True 162 | ) 163 | 164 | plot_bytes.seek(0) 165 | fig.clf() 166 | 167 | fname = f"guild-msg-activity-{ctx.guild.id}.png" 168 | plot_file = discord.File(plot_bytes, filename=fname) 169 | 170 | embed = await ctx.embed(f"Hourly Message Activity - {ctx.guild.name}", send=False) 171 | embed.set_image(url=f"attachment://{fname}") 172 | await ctx.send(file=plot_file, embed=embed) 173 | -------------------------------------------------------------------------------- /eevee/cogs/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests Features Module""" 2 | 3 | from .cog import Tests 4 | 5 | def setup(bot): 6 | bot.add_cog(Tests(bot)) 7 | -------------------------------------------------------------------------------- /eevee/cogs/tests/cog.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | import os 4 | import pkgutil 5 | import random 6 | import re 7 | 8 | import discord 9 | import numpy 10 | 11 | from discord.ext import commands 12 | 13 | from eevee import Cog, checks, command 14 | from eevee.utils.converters import Guild, Multi 15 | 16 | PBOT_APPID = 'un08c68977' 17 | PBOT_UKEY = 'd6ec6b1babce597b27050962926f3a4c' 18 | 19 | 20 | class Tests(Cog): 21 | """Test Features""" 22 | 23 | async def __local_check(self, ctx): 24 | if not self.tables: 25 | pass 26 | owner = await checks.check_is_co_owner(ctx) 27 | enabled = await checks.check_cog_enabled(ctx) 28 | return all((owner, enabled)) 29 | 30 | @command() 31 | async def log_test(self, ctx, *, log_msg): 32 | self.logger.info(log_msg) 33 | await ctx.send(f'Sent the following log msg:\n{log_msg}') 34 | 35 | @command() 36 | async def thumbtest(self, ctx, *, url=None): 37 | """Test a thumbnail image in an embed.""" 38 | attachments = ctx.message.attachments 39 | if attachments: 40 | url = attachments[0].url 41 | 42 | embed = discord.Embed() 43 | embed.set_thumbnail(url=url) 44 | try: 45 | await ctx.send(embed=embed) 46 | except discord.HTTPException: 47 | await ctx.send( 48 | "You've provided an incorrect URL, or I need the ", 49 | "`Embed links` permission to send this") 50 | 51 | @command(category="Server Config") 52 | async def _test(self, ctx): 53 | await ctx.send(f"{self.__class__.__name__}Enabled") 54 | 55 | @command() 56 | async def ok(self, ctx): 57 | await ctx.ok() 58 | 59 | @command() 60 | async def aemoji(self, ctx): 61 | """Run a command as a different member.""" 62 | await ctx.send("") 63 | 64 | @command() 65 | async def getemoji(self, ctx): 66 | """Get an emoji from any connected server.""" 67 | await ctx.send(str(ctx.guild.emojis)) 68 | 69 | def process_template(self, message, author, guild): 70 | def template_replace(match): 71 | if match.group(3): 72 | if match.group(3) == 'user': 73 | return author.mention 74 | elif match.group(3) == 'server': 75 | return guild.name 76 | else: 77 | return match.group(0) 78 | match_type = match.group(1) 79 | full_match = match.group(0) 80 | match = match.group(2) 81 | if match_type == "@": 82 | member = guild.get_member_named(match) 83 | if match.isdigit() and not member: 84 | member = guild.get_member(int(match)) 85 | return member.mention if member else full_match 86 | elif match_type == "#": 87 | channel = discord.utils.get(guild.channels, name=match) 88 | if match.isdigit() and not channel: 89 | channel = guild.get_channel(int(match)) 90 | return channel.mention if channel else full_match 91 | elif match_type == '&': 92 | role = discord.utils.get(guild.roles, name=match) 93 | if match.isdigit() and not role: 94 | role = discord.utils.get(guild.roles, id=match) 95 | return role.mention if role else full_match 96 | template_pattern = r'{(@|#|&)([^{}]+)}|{(user|server)}' 97 | return re.sub(template_pattern, template_replace, message) 98 | 99 | @command() 100 | async def template_test(self, ctx, *, msg_str): 101 | await ctx.send(self.process_template(msg_str, ctx.author, ctx.guild)) 102 | 103 | @command() 104 | async def test(self, ctx): 105 | await ctx.send('test') 106 | 107 | @command() 108 | async def set_admin(self, ctx, role: discord.Role): 109 | await ctx.setting('AdminRole', role.id) 110 | await ctx.send(f'Set {role.name} as this guilds Admin Role.') 111 | 112 | @command() 113 | async def set_mod(self, ctx, role: discord.Role): 114 | await ctx.setting('ModRole', role.id) 115 | await ctx.send(f'Set {role.name} as this guilds Mod Role.') 116 | 117 | @command() 118 | async def find_guild(self, ctx, *, guild: Guild): 119 | if guild: 120 | await ctx.send(f"{guild.name} - {guild.id}") 121 | else: 122 | await ctx.send("Guild not found.") 123 | 124 | @command() 125 | async def get_member(self, ctx, *, member: discord.Member): 126 | await ctx.send(f"{member.name} - {member.id}") 127 | 128 | @command() 129 | async def delete_msg(self, ctx, *message_ids: int): 130 | for msg_id in message_ids: 131 | msg = await ctx.get.message(id=msg_id) 132 | if not msg: 133 | return 134 | await msg.delete() 135 | await asyncio.sleep(5) 136 | try: 137 | await ctx.message.delete() 138 | except discord.HTTPException: 139 | pass 140 | 141 | @command() 142 | async def codeblock(self, ctx, syntax, *, content): 143 | await ctx.codeblock(content, syntax=syntax) 144 | 145 | @commands.command() 146 | async def horserace(self, ctx, amount: int, horse: str): 147 | """Choose a horse to bet on! 148 | 149 | Aria - 40% 150 | Bally - 30% 151 | Bellagio - 15% 152 | Flamingo - 10% 153 | Luxor - 5% 154 | """ 155 | max_bet = 100 156 | min_max_betters = (80, 100) 157 | horse_data = { 158 | 'Aria': 0.4, 159 | 'Bally': 0.3, 160 | 'Bellagio': 0.15, 161 | 'Flamingo': 0.1, 162 | 'Luxor': 0.05, 163 | } 164 | if horse not in horse_data.keys(): 165 | return await ctx.send("Please pick a valid horse.") 166 | bets = {} 167 | for rhorse in numpy.random.choice( 168 | list(horse_data.keys()), 169 | numpy.random.randint(min_max_betters[0], min_max_betters[1]), 170 | p=list(horse_data.values())): 171 | bets[rhorse] = bets.setdefault(rhorse, list()) 172 | bets[rhorse].append(numpy.random.choice(max_bet)) 173 | betters = sum([len(vals) for vals in bets.values()]) + 1 174 | bet_total = sum([sum(vals) for vals in bets.values()]) + amount 175 | first, second, third = numpy.random.choice( 176 | list(horse_data.keys()), 3, replace=False, p=list(horse_data.values())) 177 | winner_bet = bets.get(first, [0,]) 178 | win_count = len(winner_bet) + (1 if horse == first else 0) 179 | win_total = sum(winner_bet) + amount 180 | payout_pd = round((bet_total / win_total) * 0.9, 2) 181 | payout = round((bet_total * amount / win_total) * 0.9, 2) if horse == first else 0 182 | result = "Winner!" if horse == first else "Better Luck Next Time" 183 | fields = { 184 | "Results":(f"**First:** {first}\n" 185 | f"**Second:** {second}\n" 186 | f"**Third:** {third}"), 187 | "Stats" :(f"**Betters:** {betters}\n" 188 | f"**Winners:** {win_count}\n" 189 | f"**Bet Pool:** ${bet_total}\n" 190 | f"**Payout:** ${payout_pd} per $1") 191 | } 192 | await ctx.embed(f"{result}", f"You've won ${payout}", fields=fields) 193 | 194 | @command() 195 | async def tree(self, ctx): 196 | tree_dict = {} 197 | def get_modules(path): 198 | return { 199 | ext : get_modules(os.path.join(path, ext)) if ispkg else False 200 | for __, ext, ispkg in pkgutil.iter_modules([path]) 201 | } 202 | tree_dict['eevee'] = get_modules(ctx.bot.eevee_dir) 203 | tree_lines = [] 204 | def build_tree(data, level, last=False): 205 | if level == 0: 206 | padding = "" 207 | elif last and level > 2: 208 | padding = " │ "+(" "*(level-2)) 209 | elif last and level > 1: 210 | padding = " "*(level-1) 211 | else: 212 | padding = " │ "*(level-1) 213 | count = 0 214 | total = len(data) 215 | for k, v in data.items(): 216 | count += 1 217 | prefix = " ├── " 218 | if level == 0: 219 | prefix = "" 220 | elif count == total: 221 | prefix = " └── " 222 | buffer = padding+prefix 223 | if v: 224 | tree_lines.append(f"{buffer}[{k}]") 225 | build_tree(v, level+1, total == count) 226 | else: 227 | tree_lines.append(f"{buffer}{k}.py") 228 | build_tree(tree_dict, 0) 229 | await ctx.codeblock('\n'.join(tree_lines), 'css') 230 | 231 | @command() 232 | async def sql_test(self, ctx, from_msg_id: int, process: bool = False): 233 | table = ctx.bot.dbi.tablenew('command_log') 234 | msg_id = table['message_id'] 235 | auth_id = table['author_id'] 236 | table.query(msg_id.sum).order_by(msg_id, asc=False).group_by(msg_id) 237 | table.query.where(msg_id > from_msg_id) 238 | table.query.where( 239 | (auth_id == 174764205927432192, 240 | auth_id == 394529085923262464)) 241 | if process: 242 | data = await table.query.get() 243 | return await ctx.codeblock(data) 244 | sql = f"{table.query.sql[0]}\n\n-- VALUES --\n{table.query.sql[1]}" 245 | await ctx.codeblock(sql, "sql") 246 | 247 | @command() 248 | async def roll(self, ctx, *dice): 249 | def die_roll(sides: int): 250 | return random.choice(range(1, sides+1)) 251 | def process_dice(d): 252 | rolls, sides = map(int, d.split('d')) 253 | return [die_roll(sides) for i in range(rolls)] 254 | results = [] 255 | for d in dice: 256 | results.append(process_dice(d)) 257 | msg = '\n'.join(', '.join(map(str, l)) for l in results) 258 | return await ctx.embed("Results", msg) 259 | 260 | @command() 261 | async def test_default(self, ctx, guild: discord.Guild = None): 262 | guild = guild or ctx.guild 263 | names = ["general", "lounge", "chat"] 264 | def can_use(channel): 265 | if not channel.name in names: 266 | return False 267 | perms = channel.permissions_for(guild.me) 268 | return perms.send_messages and perms.read_messages 269 | results = list(filter(can_use, guild.text_channels)) 270 | await ctx.send(str(results)) 271 | 272 | @command() 273 | async def show_deleted(self, ctx, count: int = 1, 274 | channel: discord.TextChannel = None): 275 | msg_table = ctx.bot.dbi.table('discord_messages') 276 | msg_table.query( 277 | 'message_id', 'sent', 'is_edit', 'author_id', 'clean_content', 278 | 'embeds', 'attachments') 279 | msg_table.query.where(channel_id=channel or ctx.channel.id) 280 | msg_table.query.where(deleted=True) 281 | msg_table.query.order_by('message_id', 'sent', asc=False) 282 | msg_table.query.limit(count) 283 | print(msg_table.query.sql()) 284 | messages = await msg_table.query.get() 285 | 286 | if not messages: 287 | return await ctx.embed( 288 | "I didn't find any deleted messages, sorry!") 289 | 290 | msg_data = {} 291 | for msg in messages: 292 | author = await ctx.get.user(msg['author_id']) 293 | date = datetime.datetime.fromtimestamp(int(msg['sent'])) 294 | date_str = date.strftime('%Y-%m-%d %H:%M:%S') 295 | content = msg['clean_content'] 296 | embeds = msg['embeds'] 297 | attachments = msg['attachments'] 298 | if len(embeds) > 1: 299 | embed_content = f'{len(embeds)} Embeds' 300 | elif embeds: 301 | embed = embeds[0] 302 | embed_content = [] 303 | if embed.get('author'): 304 | embed_content.append( 305 | f"AuthorTitle: {embed['author']['name']}") 306 | if embed.get('title'): 307 | embed_content.append( 308 | f"Title: {embed['title']}") 309 | if embed.get('description'): 310 | embed_content.append( 311 | f"Description: {embed['description']}") 312 | if embed.get('fields'): 313 | embed_content.append( 314 | f"Field Count: {len(embed['fields'])}") 315 | if embed.get('color'): 316 | embed_content.append( 317 | f"Colour: {embed['color']}") 318 | if embed_content: 319 | embed_content = '\n'.join(embed_content) 320 | else: 321 | embed_content = '1 Embed' 322 | 323 | content += f"\n**Embeds:**\n{embed_content}" 324 | 325 | if attachments: 326 | att_content = '\n'.join(attachments) 327 | content += f"\n**Attachments:**\n{att_content}" 328 | 329 | if not content: 330 | content = "No content" 331 | 332 | msg_data[f"{author.display_name} | {date_str}"] = content 333 | 334 | if count > 1: 335 | title = 'Recently Deleted Messages' 336 | else: 337 | title = 'Last Deleted Message' 338 | 339 | await ctx.embed(title, fields=msg_data) 340 | 341 | @command() 342 | async def clonechannel(self, ctx, channel_id: int): 343 | channel = ctx.get.channel(channel_id) 344 | if not channel: 345 | return await ctx.error('No Channel Found') 346 | ch_types = { 347 | discord.TextChannel : discord.ChannelType.text, 348 | discord.VoiceChannel : discord.ChannelType.voice, 349 | discord.CategoryChannel : discord.ChannelType.category 350 | } 351 | channel_type = ch_types[type(channel)] 352 | await ctx.guild._create_channel( 353 | channel.name, dict(channel.overwrites), channel_type, 354 | channel.category, "Clone Test") 355 | 356 | @command() 357 | async def uniontest(self, ctx, test: Multi(discord.Member, discord.TextChannel, int), *, test_content=None): 358 | await ctx.send(str(type(test))) 359 | 360 | @command() 361 | async def ask_test(self, ctx, *, options: str = None): 362 | options = options.split(' ') if options else None 363 | response = await ctx.ask( 364 | 'pls confirm', timeout=10, options=options) 365 | await ctx.send(str(response)) 366 | 367 | @command() 368 | async def movechan(self, ctx, position: int, channel: discord.TextChannel): 369 | await channel.edit(position=position) 370 | await ctx.ok() 371 | 372 | @command() 373 | async def sortchan(self, ctx, reverse: bool = False): 374 | """Sort the current categories channels by alphabetical order.""" 375 | channels = enumerate(sorted( 376 | ctx.channel.category.channels, 377 | key=lambda item: item.name, 378 | reverse=reverse)) 379 | 380 | for position, channel in channels: 381 | await channel.edit(position=position) 382 | 383 | await ctx.ok() 384 | -------------------------------------------------------------------------------- /eevee/cogs/tests/tables.py: -------------------------------------------------------------------------------- 1 | from meowth.core.data_manager import schema 2 | 3 | 4 | # def setup(bot): 5 | # team_table = bot.dbi.table('teams') 6 | # team_table.new_columns = [ 7 | # schema.IDColumn('team_id', primary_key=True), 8 | # schema.IDColumn('color_id', unique=True), 9 | # schema.StringColumn('identifier', unique=True), 10 | # schema.StringColumn('emoji', unique=True) 11 | # ] 12 | 13 | # team_names_table = bot.dbi.table('team_names') 14 | # languages = bot.dbi.table('languages') 15 | # team_names_table.new_columns = [ 16 | # schema.IDColumn('team_id', primary_key=True), 17 | # schema.IDColumn('language_id', primary_key=True, 18 | # foreign_key=languages['language_id']), 19 | # schema.StringColumn('team_name') 20 | # ] 21 | 22 | # team_table.initial_data = [ 23 | # { 24 | # "team_id": 1, 25 | # "color_id": 2, 26 | # "identifier": "mystic", 27 | # "emoji": ':mystic:' 28 | # }, 29 | # { 30 | # "team_id": 2, 31 | # "color_id": 10, 32 | # "identifier": "instinct", 33 | # "emoji": ':instinct:' 34 | # }, 35 | # { 36 | # "team_id": 3, 37 | # "color_id": 8, 38 | # "identifier": "valor", 39 | # "emoji": ':valor:' 40 | # } 41 | # ] 42 | 43 | # team_names_table.initial_data = [ 44 | # { 45 | # "team_id": 1, 46 | # "language_id": 9, 47 | # "team_name": "mystic" 48 | # }, 49 | # { 50 | # "team_id": 1, 51 | # "language_id": 12, 52 | # "team_name": "mystic", 53 | # }, 54 | # { 55 | # "team_id": 1, 56 | # "language_id": 1, 57 | # "team_name": "ミスティック", 58 | # }, 59 | # { 60 | # "team_id": 1, 61 | # "language_id": 2, 62 | # "team_name": "misutikku", 63 | # }, 64 | # { 65 | # "team_id": 1, 66 | # "language_id": 5, 67 | # "team_name": "sagesse", 68 | # }, 69 | # { 70 | # "team_id": 1, 71 | # "language_id": 6, 72 | # "team_name": "weisheit", 73 | # }, 74 | # { 75 | # "team_id": 1, 76 | # "language_id": 7, 77 | # "team_name": "sabiduría", 78 | # }, 79 | # { 80 | # "team_id": 1, 81 | # "language_id": 8, 82 | # "team_name": "saggezza", 83 | # }, 84 | # { 85 | # "team_id": 2, 86 | # "language_id": 9, 87 | # "team_name": "instinct", 88 | # }, 89 | # { 90 | # "team_id": 2, 91 | # "language_id": 12, 92 | # "team_name": "instinct", 93 | # }, 94 | # { 95 | # "team_id": 2, 96 | # "language_id": 1, 97 | # "team_name": "インスティンクト", 98 | # }, 99 | # { 100 | # "team_id": 2, 101 | # "language_id": 2, 102 | # "team_name": "insutinkuto", 103 | # }, 104 | # { 105 | # "team_id": 2, 106 | # "language_id": 5, 107 | # "team_name": "intuition", 108 | # }, 109 | # { 110 | # "team_id": 2, 111 | # "language_id": 6, 112 | # "team_name": "intuition", 113 | # }, 114 | # { 115 | # "team_id": 2, 116 | # "language_id": 7, 117 | # "team_name": "instinto", 118 | # }, 119 | # { 120 | # "team_id": 2, 121 | # "language_id": 8, 122 | # "team_name": "istinto", 123 | # }, 124 | # { 125 | # "team_id": 3, 126 | # "language_id": 9, 127 | # "team_name": "valor", 128 | # }, 129 | # { 130 | # "team_id": 3, 131 | # "language_id": 12, 132 | # "team_name": "valour", 133 | # }, 134 | # { 135 | # "team_id": 3, 136 | # "language_id": 1, 137 | # "team_name": "ヴァーラー", 138 | # }, 139 | # { 140 | # "team_id": 3, 141 | # "language_id": 2, 142 | # "team_name": "ba-ra", 143 | # }, 144 | # { 145 | # "team_id": 3, 146 | # "language_id": 5, 147 | # "team_name": "bravoure", 148 | # }, 149 | # { 150 | # "team_id": 3, 151 | # "language_id": 6, 152 | # "team_name": "wagemut", 153 | # }, 154 | # { 155 | # "team_id": 3, 156 | # "language_id": 7, 157 | # "team_name": "valor", 158 | # }, 159 | # { 160 | # "team_id": 3, 161 | # "language_id": 8, 162 | # "team_name": "coraggio", 163 | # } 164 | # ] 165 | 166 | # return [team_table, team_names_table] 167 | -------------------------------------------------------------------------------- /eevee/cogs/time/__init__.py: -------------------------------------------------------------------------------- 1 | """Time Tools Module""" 2 | 3 | from .cog import Time 4 | 5 | def setup(bot): 6 | bot.add_cog(Time(bot)) 7 | -------------------------------------------------------------------------------- /eevee/cogs/time/cog.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pytz 3 | 4 | import pytzdata 5 | import pendulum 6 | import discord 7 | 8 | from eevee import command, group, Cog, checks 9 | from eevee.utils import fuzzymatch 10 | 11 | 12 | class Time(Cog): 13 | """Time Tools""" 14 | def __init__(self, bot): 15 | self.bot = bot 16 | self.tzdburl = 'https://github.com/sdispater/pytzdata/blob/master/pytzdata/_timezones.py' 17 | self.timezones = None 18 | self.get_timezones() 19 | 20 | def get_timezones(self): 21 | zone_tab = pytz.open_resource('zone.tab') 22 | try: 23 | data = {} 24 | for line in zone_tab: 25 | line = line.decode('UTF-8') 26 | if line.startswith('#'): 27 | continue 28 | code, coordinates, zone = line.split(None, 4)[:3] 29 | if zone not in pytz.all_timezones_set: 30 | continue 31 | try: 32 | data[code].append(zone) 33 | except KeyError: 34 | data[code] = [zone] 35 | self.timezones = data 36 | finally: 37 | zone_tab.close() 38 | 39 | def timezone_names(self): 40 | tznames = {} 41 | for tz in pytz.all_timezones: 42 | name = pytz.timezone(tz).localize(datetime.datetime.now()).tzname() 43 | try: 44 | tznames[name].append(tz) 45 | except KeyError: 46 | tznames[name] = [tz,] 47 | return tznames 48 | 49 | def match_timezone(self, query): 50 | # try it as-is first 51 | try: 52 | pytz.timezone(query) 53 | return [(query, 100),] 54 | except pytz.UnknownTimeZoneError: 55 | pass 56 | 57 | # check if in tz names 58 | tz_names = self.timezone_names() 59 | if query.upper() in tz_names: 60 | return [(tz, 100) for tz in tz_names[query.upper()]] 61 | 62 | # fuzzymatch against all timezones as last resort 63 | matches = fuzzymatch.get_matches(tz_names.keys(), query, 80) 64 | 65 | commontz_matches = fuzzymatch.get_matches( 66 | pytz.common_timezones, query, 90, True) 67 | 68 | if commontz_matches: 69 | matches.extend(commontz_matches) 70 | 71 | fullmatches = [(tz, 100) for tz, s in matches if s == 100] 72 | if len(fullmatches) == 1: 73 | return fullmatches 74 | 75 | return matches 76 | 77 | async def get_timezone(self, member_id): 78 | table = self.bot.dbi.table('member_timezones') 79 | table.query('timezone') 80 | table.query.where(member_id=member_id) 81 | return await table.query.get_value() 82 | 83 | async def verify_timezone(self, ctx, timezone): 84 | 85 | # detect utc offset values 86 | try: 87 | tzoffset = datetime.timedelta(hours=float(timezone)) 88 | datetime.timezone(tzoffset) 89 | except (ValueError, TypeError): 90 | pass 91 | else: 92 | return timezone 93 | 94 | # try matching if not an offset 95 | results = self.match_timezone(timezone) 96 | 97 | if not results: 98 | return None 99 | 100 | if len(results) == 1: 101 | return results[0][0] 102 | 103 | result_str = [f"{tz} ({s}%)" for tz, s in results] 104 | results = [tz for tz, s in results] 105 | 106 | which_msg = await ctx.embed( 107 | "Which of these matches?", 108 | '\n'.join(result_str), 109 | msg_type='help', 110 | footer="To stop, reply with 'cancel'.") 111 | 112 | def check(m): 113 | return m.channel == ctx.channel and m.author == ctx.author 114 | 115 | reply_msg = await ctx.bot.wait_for('message', check=check) 116 | 117 | await which_msg.delete() 118 | 119 | if reply_msg.content.lower() == 'cancel': 120 | return None 121 | 122 | return fuzzymatch.get_match(results, reply_msg.content, 95, True)[0] 123 | 124 | @command() 125 | async def tztest(self, ctx, *, timezone): 126 | 127 | match = await self.verify_timezone(ctx, timezone) 128 | 129 | if not match: 130 | return await ctx.error('No Match') 131 | 132 | tz_time = pendulum.now(match).format('HH:mm, dddd') 133 | 134 | await ctx.success(f'Match: {match} | {tz_time}') 135 | 136 | @group(invoke_without_command=True, aliases=['timezone']) 137 | async def tz(self, ctx, member: discord.Member = None): 138 | """Shows a member's timezone""" 139 | member = member or ctx.author 140 | timezone = await self.get_timezone(member.id) 141 | if not timezone: 142 | return await ctx.error( 143 | f'{member.display_name} has not set a timezone yet.', 144 | f'To set one, use the `{ctx.prefix}tz set` command like so:\n' 145 | f'```{ctx.prefix}tz set US/Eastern```\n' 146 | f"[List of all available timezones]({self.tzdburl})") 147 | return await ctx.embed(f"{member.display_name}'s Timezone", timezone) 148 | 149 | @tz.command(name='list') 150 | async def tz_list(self, ctx): 151 | await ctx.embed("Available Timezones List", title_url=self.tzdburl) 152 | 153 | @tz.command(name='set') 154 | async def _set(self, ctx, timezone=None, member: discord.Member = None): 155 | """Sets a member's timezone. Only co-owners can set other's tzs.""" 156 | 157 | if member and not await checks.check_is_co_owner(ctx): 158 | return await ctx.error('You can only set your own timezone.') 159 | 160 | member = member or ctx.author 161 | 162 | if timezone: 163 | timezone = await self.verify_timezone(ctx, timezone) 164 | if not timezone: 165 | return await ctx.error('Invalid Timezone.') 166 | 167 | table = ctx.bot.dbi.table('member_timezones') 168 | table.insert( 169 | member_id=member.id, 170 | timezone=str(timezone)) 171 | table.insert.primaries('member_id') 172 | await table.insert.commit(do_update=True) 173 | 174 | await ctx.success( 175 | f'Timezone for {member.display_name} saved as {timezone}.') 176 | 177 | @tz.command(name='rm', aliases=['remove', 'del', 'delete', 'clear']) 178 | async def _rm(self, ctx, member: discord.Member = None): 179 | """Sets a member's timezone""" 180 | if member and not await checks.check_is_co_owner(ctx): 181 | return await ctx.error('You can only remove your own timezone.') 182 | member = member or ctx.author 183 | 184 | query = ctx.bot.dbi.table('member_timezones').query 185 | await query.delete(member_id=member.id) 186 | 187 | await ctx.success( 188 | f'Timezone for {member.display_name} removed.') 189 | 190 | @group(invoke_without_command=True) 191 | async def time(self, ctx, member: discord.Member = None): 192 | """Shows a member's current local time.""" 193 | member = member or ctx.author 194 | timezone = await self.get_timezone(member.id) 195 | if not timezone or timezone == "None": 196 | if ctx.author == member: 197 | return await ctx.error( 198 | f'{member.display_name} has not set a timezone yet.', 199 | f'To set one, use the `{ctx.prefix}tz set` command like so:\n' 200 | f'```{ctx.prefix}tz set US/Eastern```\n' 201 | f"[List of all available timezones]({self.tzdburl})") 202 | return await ctx.error(f'{member.display_name} has not set a timezone yet.') 203 | try: 204 | timezone = float(timezone) 205 | except ValueError: 206 | pass 207 | if isinstance(timezone, float): 208 | tzoffset = datetime.timedelta(hours=timezone) 209 | tz = datetime.datetime.utcnow() + tzoffset 210 | tz = tz.strftime('%H:%M, %A') 211 | else: 212 | tz = pendulum.now(timezone).format('HH:mm, dddd') 213 | await ctx.embed(f'Time for {member.display_name}', tz, footer=timezone) 214 | 215 | @time.command(name='tz', aliases=['timezones', 'timezone']) 216 | async def time_tz(self, ctx, timezone=None): 217 | timezone = timezone or 'UTC' 218 | timezone = await self.verify_timezone(ctx, timezone) 219 | try: 220 | timezone = float(timezone) 221 | except ValueError: 222 | pass 223 | if isinstance(timezone, float): 224 | tzoffset = datetime.timedelta(hours=timezone) 225 | tz = datetime.datetime.utcnow() + tzoffset 226 | tz = tz.strftime('%H:%M, %A') 227 | else: 228 | tz = pendulum.now(timezone).format('HH:mm, dddd') 229 | await ctx.embed(f'Time for {timezone}', tz) 230 | -------------------------------------------------------------------------------- /eevee/cogs/trainers/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import Trainers 2 | 3 | def setup(bot): 4 | bot.add_cog(Trainers(bot)) 5 | -------------------------------------------------------------------------------- /eevee/cogs/trainers/cog.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | from eevee import group, Cog 4 | 5 | from .objects import Trainer 6 | 7 | class Trainers(Cog): 8 | """Trainer Features""" 9 | 10 | async def set_trainer(self, member, **data): 11 | return await Trainer.put(self.bot, member, **data) 12 | 13 | async def get_trainer(self, member): 14 | return await Trainer.get(self.bot, member) 15 | 16 | @group(invoke_without_command=True) 17 | async def trainer(self, ctx, trainer: discord.Member): 18 | trainer = await self.get_trainer(trainer) 19 | await ctx.send(f'Trainer: {trainer}\n' 20 | f'Trainer ID: {trainer.id}\n' 21 | f'Team: {trainer.team}\n' 22 | f'Silph ID: {trainer.silph_id}\n' 23 | f'Pokebattler ID: {trainer.pokebattler_id}') 24 | 25 | @trainer.command(name="set") 26 | async def _set(self, ctx, trainer: discord.Member, silph_id: str = None, 27 | pokebattler_id: int = None, team: str = None): 28 | 29 | data = dict(silph_id=silph_id, 30 | pokebattler_id=pokebattler_id, 31 | team=team) 32 | 33 | data = {k:v for k, v in data.items() if v} 34 | 35 | trainer = await self.set_trainer(trainer, **data) 36 | 37 | await ctx.send(f'Trainer: {trainer}\n' 38 | f'Trainer ID: {trainer.id}\n' 39 | f'Team: {trainer.team}\n' 40 | f'Silph ID: {trainer.silph_id}\n' 41 | f'Pokebattler ID: {trainer.pokebattler_id}') 42 | 43 | @trainer.command() 44 | async def remove(self, ctx, trainer: discord.Member): 45 | trainer = await self.get_trainer(trainer) 46 | await trainer.delete() 47 | await ctx.send(f"{trainer}'s trainer data deleted.") 48 | -------------------------------------------------------------------------------- /eevee/cogs/trainers/objects.py: -------------------------------------------------------------------------------- 1 | from discord.ext.commands import MemberConverter 2 | 3 | class Trainer: 4 | """Represents a user of Meowth, with their relevant persistant info.""" 5 | 6 | __slots__ = ('bot', 'member', '_data', 'table') 7 | 8 | def __init__(self, bot, member): 9 | self.bot = bot 10 | self.member = member 11 | self._data = {} 12 | self.table = bot.dbi.table('trainers').where(trainer_id=self.id) 13 | 14 | def __str__(self): 15 | return self.member.__str__() 16 | 17 | def __getattr__(self, name): 18 | return getattr(self.member, name, None) 19 | 20 | @property 21 | def silph_id(self): 22 | return self._data.get('silph_id', None) 23 | 24 | @property 25 | def pokebattler_id(self): 26 | return self._data.get('pokebattler_id', None) 27 | 28 | @property 29 | def team(self): 30 | return self._data.get('team', None) 31 | 32 | async def update_data(self, **data): 33 | if data: 34 | self._data = dict(self._data, **data) 35 | await self.table.upsert(**self._data) 36 | 37 | async def get_data(self): 38 | data = await self.table.get_first() 39 | self._data = data if data else {} 40 | 41 | async def delete(self): 42 | await self.table.delete() 43 | 44 | @classmethod 45 | async def convert(cls, ctx, argument): 46 | member = await MemberConverter.convert(ctx, argument) 47 | instance = cls(ctx.bot, member) 48 | await instance.get_data() 49 | return instance if member else None 50 | 51 | @classmethod 52 | async def get(cls, bot, member): 53 | instance = cls(bot, member) 54 | await instance.get_data() 55 | return instance 56 | 57 | @classmethod 58 | async def put(cls, bot, member, **data): 59 | instance = cls(bot, member) 60 | if data: 61 | await instance.update_data(**data) 62 | return instance 63 | -------------------------------------------------------------------------------- /eevee/cogs/trainers/tables.py: -------------------------------------------------------------------------------- 1 | from eevee.core.data_manager import schema 2 | 3 | def setup(bot): 4 | cog_table = bot.dbi.table('trainers') 5 | cog_table.new_columns = [ 6 | schema.IDColumn('trainer_id', primary_key=True), 7 | schema.StringColumn('silph_id'), 8 | schema.IntColumn('pokebattler_id'), 9 | schema.IntColumn('team', small=True) 10 | ] 11 | return cog_table 12 | -------------------------------------------------------------------------------- /eevee/cogs/xkcd/__init__.py: -------------------------------------------------------------------------------- 1 | """Public Tests Features Module""" 2 | 3 | from .cog import XKCD 4 | 5 | def setup(bot): 6 | bot.add_cog(XKCD(bot)) 7 | -------------------------------------------------------------------------------- /eevee/cogs/xkcd/cog.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from datetime import datetime 3 | 4 | import asyncpg 5 | from async_timeout import timeout 6 | 7 | from eevee import command, Cog, group 8 | 9 | LATEST_URL = "https://xkcd.com/info.0.json" 10 | ISSUE_URL = "https://xkcd.com/{comic_num}/info.0.json" 11 | 12 | 13 | class XKCD(Cog): 14 | """Test commands that are open for public usage.""" 15 | def __init__(self, bot): 16 | self.bot = bot 17 | self.table = bot.dbi.table('xkcd') 18 | self.update_task = None 19 | bot.loop.create_task(self.prepare_db()) 20 | 21 | async def prepare_db(self): 22 | try: 23 | await self.bot.dbi.execute_query("CREATE EXTENSION fuzzystrmatch;") 24 | except asyncpg.DuplicateObjectError: 25 | pass 26 | 27 | async def get_comic(self, issue: int = None): 28 | if issue in [404]: 29 | return None 30 | 31 | url = ISSUE_URL.format(comic_num=issue) if issue else LATEST_URL 32 | 33 | async with timeout(10): 34 | async with self.bot.session.get(url) as r: 35 | return await r.json() 36 | 37 | async def latest_id(self): 38 | data = await self.get_comic() 39 | return data["num"] 40 | 41 | async def update_data(self, feedback_dest=None): 42 | if feedback_dest: 43 | await feedback_dest.send("Starting Update") 44 | query = self.table.query("id").order_by("id", asc=False).limit(1) 45 | data = await query.get_one() 46 | if data: 47 | result = data["id"] 48 | else: 49 | result = 0 50 | 51 | if feedback_dest: 52 | await feedback_dest.send(f"Latest Stored Comic ID: {result}") 53 | 54 | latest = await self.latest_id() 55 | if feedback_dest: 56 | await feedback_dest.send(f"Latest Released Comic ID: {latest}") 57 | 58 | if result >= latest: 59 | if feedback_dest: 60 | await feedback_dest.send(f"Update Finished: No new releases found.") 61 | return 62 | 63 | if feedback_dest: 64 | update_text = f"Pulling updates for comics {result} to {latest}." 65 | update_msg = await feedback_dest.send(update_text + f"\n{result}/{latest} done.") 66 | 67 | for i in range(result+1, latest+1): 68 | data = await self.get_comic(i) 69 | 70 | if not data: 71 | continue 72 | 73 | self.table.insert.row( 74 | id=int(data['num']), 75 | img=data['img'], 76 | title=data['title'], 77 | safe_title=data['safe_title'], 78 | alt=data['alt'], 79 | year=int(data['year']), 80 | month=int(data['month']), 81 | day=int(data['day']), 82 | transcript=data['transcript'], 83 | news=data['news'] 84 | ) 85 | 86 | await self.table.insert.commit(do_update=False) 87 | 88 | if feedback_dest: 89 | last_change = update_msg.edited_at or update_msg.created_at 90 | since_change = datetime.utcnow() - last_change 91 | if since_change.total_seconds() > 5: 92 | await update_msg.edit(content=update_text + f"\n{data['num']}/{latest} done.") 93 | 94 | if feedback_dest: 95 | await update_msg.edit(content=update_text + f"\n{latest}/{latest} done.") 96 | await feedback_dest.send(f"Updated Complete.") 97 | 98 | self.update_task = None 99 | 100 | @group(invoke_without_command=True) 101 | async def xkcd(self, ctx, comic_number: typing.Optional[int]): 102 | data = await self.get_comic(comic_number) 103 | if not data: 104 | return await ctx.error("Invalid XKCD number.") 105 | 106 | title = (f"{data['safe_title']} - " 107 | f"{data['num']} - " 108 | f"{data['year']}/{data['month']}/{data['day']}") 109 | await ctx.embed(title, footer=data['alt'], image=data['img']) 110 | 111 | def cancel_task(self): 112 | if not self.update_task.done(): 113 | self.update_task.cancel() 114 | self.update_task = None 115 | 116 | @xkcd.command() 117 | async def update(self, ctx): 118 | if self.update_task: 119 | return await ctx.send("An update is already in progress.") 120 | self.update_task = ctx.bot.loop.create_task(self.update_data(ctx)) 121 | 122 | @xkcd.command() 123 | async def cancel(self, ctx): 124 | if not self.update_task: 125 | return await ctx.send("No update task running.") 126 | task = self.update_task 127 | self.cancel_task() 128 | await ctx.send("Update task cancelled.") 129 | await task 130 | 131 | @xkcd.command() 132 | async def search(self, ctx, *, search_terms): 133 | results = await ctx.bot.dbi.execute_query( 134 | "SELECT id, safe_title, year, month, day, img, alt, " 135 | "levenshtein(safe_title, $1) AS distance " 136 | "FROM xkcd ORDER BY distance ASC LIMIT 4;", 137 | search_terms 138 | ) 139 | 140 | best_result = results[0] 141 | title = ( 142 | f"{best_result['safe_title']} - " 143 | f"{best_result['id']} - " 144 | f"{best_result['year']}/{best_result['month']}/{best_result['day']}" 145 | ) 146 | await ctx.embed(title, footer=best_result['alt'], image=best_result['img']) 147 | -------------------------------------------------------------------------------- /eevee/cogs/xkcd/tables.py: -------------------------------------------------------------------------------- 1 | from eevee.core.data_manager import schema 2 | 3 | 4 | def setup(bot): 5 | table = bot.dbi.table('xkcd') 6 | table.new_columns = [ 7 | schema.IntColumn('id', primary_key=True), 8 | schema.StringColumn('img'), 9 | schema.StringColumn('title'), 10 | schema.StringColumn('safe_title'), 11 | schema.StringColumn('alt'), 12 | schema.IntColumn('year'), 13 | schema.IntColumn('month'), 14 | schema.IntColumn('day'), 15 | schema.StringColumn('transcript'), 16 | schema.StringColumn('news') 17 | ] 18 | return table 19 | -------------------------------------------------------------------------------- /eevee/config_template.py: -------------------------------------------------------------------------------- 1 | '''Configuration values for Eevee - Rename to config.py''' 2 | 3 | # bot token from discord developers 4 | bot_token = 'your_token_here' 5 | 6 | # default bot settings 7 | bot_prefix = '!' 8 | bot_master = 12345678903216549878 9 | bot_coowners = [132314336914833409, 263607303096369152] 10 | preload_extensions = [ 11 | 'tests', 12 | 'teams', 13 | 'dev', 14 | 'pokemon', 15 | 'pokebattler' 16 | ] 17 | 18 | # minimum required permissions for bot user 19 | bot_permissions = 268822608 20 | 21 | # postgresql database credentials 22 | db_details = { 23 | # 'username' : 'eevee', 24 | # 'database' : 'eevee', 25 | # 'hostname' : 'localhost', 26 | 'password' : 'password' 27 | } 28 | 29 | # default language 30 | lang_bot = 'en' 31 | lang_pkmn = 'en' 32 | 33 | # team settings 34 | team_list = ['mystic', 'valor', 'instinct'] 35 | team_colours = { 36 | "mystic" : "0x3498db", 37 | "valor" : "0xe74c3c", 38 | "instinct" : "0xf1c40f" 39 | } 40 | team_emoji = { 41 | "mystic" : "<:mystic:351758303912656896>", 42 | "valor" : "<:valor:351758298975830016>", 43 | "instinct" : "<:instinct:351758298627702786>" 44 | } 45 | 46 | # raid settings 47 | allow_assume = { 48 | "5" : "False", 49 | "4" : "False", 50 | "3" : "False", 51 | "2" : "False", 52 | "1" : "False" 53 | } 54 | status_emoji = { 55 | "omw" : ":omw:", 56 | "here_id" : ":here:" 57 | } 58 | type_emoji = { 59 | "normal" : "<:normal:351758296409178112>", 60 | "fire" : "<:fire1:351758296044142624>", 61 | "water" : "<:water:351758295142498325>", 62 | "electric" : "<:electric:351758295414865921>", 63 | "grass" : "<:grass:351758295729700868>", 64 | "ice" : "<:ice:351758296111120384>", 65 | "fighting" : "<:fighting:351758296090148864>", 66 | "poison" : "<:poison:351758295976902679>", 67 | "ground" : "<:ground:351758295968776194>", 68 | "flying" : "<:flying:351758295033446400>", 69 | "psychic" : "<:psychic:351758294744039426>", 70 | "bug" : "<:bug1:351758295196893185>", 71 | "rock" : "<:rock:351758296077697024>", 72 | "ghost" : "<:ghost1:351758295683432449>", 73 | "dragon" : "<:dragon:351758295612129280>", 74 | "dark" : "<:dark:351758294316089356>", 75 | "steel" : "<:steel:351758296425955328>", 76 | "fairy" : "<:fairy:351758295070932992>" 77 | } 78 | 79 | # help command categories 80 | command_categories = { 81 | "Owner" : { 82 | "index" : "5", 83 | "description" : "Owner-only commands for bot config or info." 84 | }, 85 | "Server Config" : { 86 | "index" : "10", 87 | "description" : "Server configuration commands." 88 | }, 89 | "Bot Info" : { 90 | "index" : "15", 91 | "description" : "Commands for finding out information on the bot." 92 | }, 93 | } 94 | 95 | # analytics/statistics 96 | pokebattler_tracker = "EeveeSelfHoster" 97 | -------------------------------------------------------------------------------- /eevee/core/__init__.py: -------------------------------------------------------------------------------- 1 | """Core contains all the required modules for getting the bot online and 2 | running. 3 | 4 | Extension management, basic error handling, statistics, commands for the bot 5 | and other inbuilt features are defined here. 6 | """ 7 | -------------------------------------------------------------------------------- /eevee/core/bot.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import os 3 | import platform 4 | import logging 5 | from collections import Counter 6 | from datetime import datetime 7 | 8 | import aiohttp 9 | import pkg_resources 10 | from dateutil.relativedelta import relativedelta 11 | 12 | import discord 13 | from discord.utils import cached_property 14 | from discord.ext import commands 15 | 16 | from eevee import config 17 | from eevee.core.context import Context 18 | from eevee.core.data_manager import DatabaseInterface, DataManager 19 | from eevee.utils import ExitCodes, pagination, fuzzymatch, make_embed 20 | 21 | 22 | class Eevee(commands.AutoShardedBot): 23 | """Represents the bot. 24 | 25 | Bases: :class:`discord.ext.commands.AutoShardedBot`. 26 | 27 | The ``AutoShardedBot`` subclass provides the benefits of 28 | :class:`discord.ext.commands.Bot` while handling the complications 29 | of sharding. 30 | 31 | Parameters 32 | ----------- 33 | description: :class:`str` 34 | A short description of the bot. 35 | launcher: :class:`bool` 36 | Flag indicating if the bot was started via launcher. 37 | debug: :class:`bool` 38 | Flag indicating if the bot was started in debug mode via CLI arg. 39 | 40 | Attributes 41 | ----------- 42 | command_prefix: :obj:`callable` 43 | Holds the Prefix Manager method for determining the guild prefix for 44 | commands. 45 | description : :class:`str` 46 | The content prefixed into the default help message. 47 | owner_id: :class:`int` 48 | The user ID of the bot owner to enable owner only commands for them. 49 | co_owners: :class:`list` 50 | List of co-owner user IDs to enable co-owner only commands for them. 51 | dbi: :class:`eevee.core.data_manager.dbi.DatabaseInterface` 52 | The interface for interacting directly with the database. 53 | data: :py:class:`.DataManager` 54 | The interface for getting and updating common data in the database. 55 | """ 56 | 57 | def __init__(self, **kwargs): 58 | self.default_prefix = config.bot_prefix 59 | self.prefixes = {} 60 | self.owner = config.bot_master 61 | self.shutdown_mode = ExitCodes.CRITICAL 62 | self.launcher = kwargs.pop('launcher') 63 | self.debug = kwargs.pop('debug') 64 | self.from_restart = kwargs.pop('from_restart') 65 | self.counter = Counter() 66 | self.launch_time = None 67 | self.core_dir = os.path.dirname(os.path.realpath(__file__)) 68 | self.eevee_dir = os.path.dirname(self.core_dir) 69 | self.data_dir = os.path.join(self.eevee_dir, "data") 70 | self.ext_dir = os.path.join(self.eevee_dir, "cogs") 71 | self.config = config 72 | self.token = config.bot_token 73 | self.req_perms = discord.Permissions(config.bot_permissions) 74 | self.co_owners = config.bot_coowners 75 | self.language = config.lang_bot 76 | self.pkmn_language = config.lang_pkmn 77 | self.preload_ext = config.preload_extensions 78 | self.dbi = DatabaseInterface(**config.db_details) 79 | self.data = DataManager(self.dbi) 80 | kwargs = dict(owner_id=self.owner, 81 | command_prefix=self.prefix_manager, 82 | status=discord.Status.dnd, **kwargs) 83 | super().__init__(**kwargs) 84 | self.session = aiohttp.ClientSession(loop=self.loop) 85 | self.loop.run_until_complete(self._db_connect()) 86 | self.logger = logging.getLogger('eevee.Eevee') 87 | 88 | async def set_prefix(self, guild_id, new_prefix=None): 89 | prefix_table = self.dbi.table('prefix') 90 | if new_prefix: 91 | prefix_table.insert.primaries('guild_id') 92 | prefix_table.insert(guild_id=guild_id, prefix=new_prefix) 93 | await prefix_table.insert.commit(do_update=True) 94 | self.prefixes[guild_id] = new_prefix 95 | else: 96 | await prefix_table.query.where(guild_id=guild_id).delete() 97 | 98 | async def load_prefixes(self): 99 | prefix_table = self.dbi.table('prefix') 100 | results = await prefix_table.query.get() 101 | self.prefixes = dict(results) 102 | 103 | async def prefix_manager(self, bot, message): 104 | """Returns the bot prefixes by context. 105 | 106 | Returns a guild-specific prefix if it has been set. If not, 107 | returns the default prefix. 108 | """ 109 | prefix = bot.prefixes.get(message.guild.id, bot.default_prefix) 110 | return commands.when_mentioned_or(prefix)(bot, message) 111 | 112 | async def _db_connect(self): 113 | await self.dbi.start(loop=self.loop) 114 | await self.load_prefixes() 115 | 116 | async def send_cmd_help(self, ctx, **kwargs): 117 | """Function to invoke help output for a command. 118 | 119 | Parameters 120 | ----------- 121 | ctx: :class:`discord.ext.commands.Context` 122 | Context object from the originally invoked command. 123 | per_page: :class:`int` 124 | Number of entries in the help embed page. 12 is default. 125 | title: :class:`str` 126 | Title of the embed message. 127 | """ 128 | try: 129 | if ctx.invoked_subcommand: 130 | kwargs['title'] = kwargs.get('title', 'Sub-Command Help') 131 | p = await pagination.Pagination.from_command( 132 | ctx, ctx.invoked_subcommand, **kwargs) 133 | else: 134 | kwargs['title'] = kwargs.get('title', 'Command Help') 135 | p = await pagination.Pagination.from_command( 136 | ctx, ctx.command, **kwargs) 137 | await p.paginate() 138 | except discord.DiscordException as exc: 139 | await ctx.send(exc) 140 | 141 | async def shutdown(self, *, restart=False): 142 | """Shutdown the bot. 143 | 144 | Safely ends the bot connection while passing the exit code based 145 | on if the intention was to restart or close. 146 | """ 147 | if not restart: 148 | self.shutdown_mode = ExitCodes.SHUTDOWN 149 | else: 150 | self.shutdown_mode = ExitCodes.RESTART 151 | await self.logout() 152 | await self.dbi.stop() 153 | 154 | @cached_property 155 | def invite_url(self): 156 | invite_url = discord.utils.oauth_url(self.user.id, 157 | permissions=self.req_perms) 158 | return invite_url 159 | 160 | @property 161 | def avatar(self): 162 | return self.user.avatar_url_as(static_format='png') 163 | 164 | @property 165 | def avatar_small(self): 166 | return self.user.avatar_url_as(static_format='png', size=64) 167 | 168 | @property 169 | def uptime(self): 170 | return relativedelta(datetime.utcnow(), self.launch_time) 171 | 172 | @property 173 | def uptime_str(self): 174 | uptime = self.uptime 175 | year_str, month_str, day_str, hour_str = ('',)*4 176 | if uptime.years >= 1: 177 | year_str = "{0}y ".format(uptime.years) 178 | if uptime.months >= 1 or year_str: 179 | month_str = "{0}m ".format(uptime.months) 180 | if uptime.days >= 1 or month_str: 181 | d_unit = 'd' if month_str else ' days' 182 | day_str = "{0}{1} ".format(uptime.days, d_unit) 183 | if uptime.hours >= 1 or day_str: 184 | h_unit = ':' if month_str else ' hrs' 185 | hour_str = "{0}{1}".format(uptime.hours, h_unit) 186 | m_unit = '' if month_str else ' mins' 187 | mins = uptime.minutes if month_str else ' {0}'.format(uptime.minutes) 188 | secs = '' if day_str else ' {0} secs'.format(uptime.seconds) 189 | min_str = "{0}{1}{2}".format(mins, m_unit, secs) 190 | 191 | uptime_str = ''.join((year_str, month_str, day_str, hour_str, min_str)) 192 | 193 | return uptime_str 194 | 195 | @property 196 | def command_count(self): 197 | return self.counter["processed_commands"] 198 | 199 | @property 200 | def message_count(self): 201 | return self.counter["messages_read"] 202 | 203 | @property 204 | def resumed_count(self): 205 | return self.counter["sessions_resumed"] 206 | 207 | def get_category(self, category): 208 | def sortkey(cmd): 209 | categories = self.config.command_categories 210 | category = getattr(cmd.callback, 'command_category', None) 211 | cat_cfg = categories.get(category) 212 | category = cat_cfg["index"] if cat_cfg else category 213 | return category or '\u200b' 214 | def groupkey(cmd): 215 | category = getattr(cmd.callback, 'command_category', None) 216 | return category or '\u200b' 217 | entries = sorted(self.commands, key=sortkey) 218 | categories = [] 219 | for cmd_group, __ in itertools.groupby(entries, key=groupkey): 220 | if cmd_group != '\u200b': 221 | categories.append(cmd_group) 222 | return category if category in categories else None 223 | 224 | async def process_commands(self, message): 225 | """Processes commands that are registed with the bot and it's groups. 226 | 227 | Without this being run in the main `on_message` event, commands will 228 | not be processed. 229 | """ 230 | if message.author.bot: 231 | return 232 | ctx = await self.get_context(message, cls=Context) 233 | if not ctx.command: 234 | return 235 | await self.invoke(ctx) 236 | 237 | def match(self, data_list, item): 238 | result = fuzzymatch.get_match(data_list, item)[0] 239 | if not result: 240 | return None 241 | return result 242 | 243 | def get(self, iterable, **attrs): 244 | """A helper that returns the first element in an iterable that meets 245 | all the attributes passed in `attrs`. 246 | """ 247 | return discord.utils.get(iterable, **attrs) 248 | 249 | def find_guild(self, name): 250 | """A helper that searches for a guild by name.""" 251 | result = self.get(self.guilds, name=name) 252 | if not result: 253 | guild_list = (guild.name for guild in self.guilds) 254 | result = self.match(guild_list, name) 255 | if not result: 256 | return None 257 | else: 258 | result = self.get(self.guilds, name=result) 259 | return result 260 | 261 | @cached_property 262 | def version(self): 263 | return pkg_resources.get_distribution("eevee").version 264 | 265 | @cached_property 266 | def py_version(self): 267 | return platform.python_version() 268 | 269 | @cached_property 270 | def dpy_version(self): 271 | return pkg_resources.get_distribution("discord.py").version 272 | 273 | @cached_property 274 | def platform(self): 275 | return platform.platform() 276 | 277 | # events 278 | async def on_message(self, message): 279 | self.counter["messages_read"] += 1 280 | await self.process_commands(message) 281 | 282 | async def on_resumed(self): 283 | self.counter["sessions_resumed"] += 1 284 | 285 | async def on_command(self, ctx): 286 | self.counter["received_commands"] += 1 287 | 288 | async def on_command_completion(self, ctx): 289 | self.counter["processed_commands"] += 1 290 | 291 | async def on_connect(self): 292 | print("Connected.") 293 | await self.change_presence(status=discord.Status.idle) 294 | 295 | # async def on_message_edit(self, before: discord.Message, after: discord.Message): 296 | # if before.content != after.content: 297 | # await self.process_commands(after) 298 | 299 | async def on_shard_ready(self, shard_id): 300 | await self.change_presence(status=discord.Status.online, shard_id=shard_id) 301 | print(f'Shard {shard_id} is ready.') 302 | 303 | async def on_ready(self): 304 | intro = "Eevee - Pokemon Go Bot for Discord" 305 | intro_deco = "{0}\n{1}\n{0}".format('='*len(intro), intro) 306 | if not self.launch_time: 307 | self.launch_time = datetime.utcnow() 308 | if not self.launcher: 309 | print(intro_deco) 310 | if self.from_restart: 311 | print("We're back!\n") 312 | else: 313 | print("We're on!\n") 314 | print(f"Eevee Version: {self.version}\n") 315 | if self.debug: 316 | print(f"Python Version: {self.py_version}") 317 | print(f"Discord.py Version: {self.dpy_version}") 318 | print(f"Platform: {self.platform}\n") 319 | guilds = len(self.guilds) 320 | users = sum([g.member_count for g in self.guilds]) 321 | if guilds: 322 | print(f"Servers: {guilds}") 323 | print(f"Members: {users}") 324 | else: 325 | print("I'm not in any server yet, so be sure to invite me!") 326 | if self.invite_url: 327 | print(f"\nInvite URL: {self.invite_url}\n") 328 | 329 | if self.from_restart: 330 | table = self.dbi.table('restart_savedata') 331 | table.query.order_by(table['restart_snowflake'], asc=False) 332 | table.query.limit(1) 333 | last_restart = (await table.query.get())[0] 334 | 335 | embed = make_embed(title='Restart Complete.', msg_type='success') 336 | 337 | guild = self.get_guild(last_restart['restart_guild']) 338 | 339 | if guild: 340 | channel = guild.get_channel(last_restart['restart_channel']) 341 | 342 | if channel: 343 | original_message = await channel.get_message( 344 | last_restart['restart_message']) 345 | return await original_message.edit(embed=embed) 346 | 347 | else: 348 | channel = self.get_user(last_restart['restart_by']) 349 | 350 | if not channel: 351 | channel = self.get_user(self.owner) 352 | if not channel: 353 | return self.logger.error('Bot owner not found.') 354 | 355 | return await channel.send(embed=embed) 356 | 357 | # command decorators 358 | 359 | def command(*args, **kwargs): 360 | """A decorator that adds a function as a bot command. 361 | 362 | For the parameters, only `name` can be positional. The rest are 363 | keyword-only. 364 | 365 | Parameters 366 | ----------- 367 | name: :class:`str` 368 | The name of the command. 369 | aliases: :class:`list` 370 | The list of other command names this will work with. 371 | enabled: :class:`bool` 372 | If `False` the command is disabled and will raise 373 | :exc:`discord.ext.commands.DisabledCommand`. 374 | hidden: :class:`bool` 375 | If `True` the command won't show in help command lists. 376 | ignore_extra: :class:`bool` 377 | If `False` any extra arguments raise 378 | :exc:`discord.ext.commands.TooManyArguments`. 379 | """ 380 | def decorator(func): 381 | category = kwargs.get("category") 382 | func.command_category = category 383 | result = commands.command(*args, **kwargs)(func) 384 | return result 385 | return decorator 386 | 387 | def group(*args, **kwargs): 388 | """A decorator that adds a function as a bot command group. 389 | 390 | These allow for easy subcommands using `funcname.command()`. 391 | 392 | For the parameters, only `name` can be positional. The rest are 393 | keyword-only. 394 | 395 | Parameters 396 | ----------- 397 | name: :class:`str` 398 | The name of the command. 399 | aliases: :class:`list` 400 | The list of other command names this will work with. 401 | enabled: :class:`bool` 402 | If `False` the command is disabled and will raise 403 | :exc:`discord.ext.commands.DisabledCommand`. 404 | hidden: :class:`bool` 405 | If `True` the command won't show in help command lists. 406 | ignore_extra: :class:`bool` 407 | If `False` any extra arguments raise 408 | :exc:`discord.ext.commands.TooManyArguments`. 409 | invoke_without_command: :class:`bool` 410 | If ``True``, invoked subcommands will skip the group commands 411 | checks and argument parsing, instead moving directly to the sub. 412 | """ 413 | def decorator(func): 414 | category = kwargs.get("category") 415 | func.command_category = category 416 | result = commands.group(*args, **kwargs)(func) 417 | return result 418 | return decorator 419 | -------------------------------------------------------------------------------- /eevee/core/checks.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | 3 | async def check_is_owner(ctx): 4 | return await ctx.bot.is_owner(ctx.author) 5 | 6 | async def check_is_co_owner(ctx): 7 | if await check_is_owner(ctx): 8 | return True 9 | if ctx.author.id in ctx.bot.co_owners: 10 | return True 11 | return False 12 | 13 | async def check_is_guildowner(ctx): 14 | if await check_is_co_owner(ctx): 15 | return True 16 | if ctx.author.id == ctx.guild.owner.id: 17 | return True 18 | return False 19 | 20 | async def check_is_admin(ctx): 21 | if await check_is_guildowner(ctx): 22 | return True 23 | if ctx.author.guild_permissions.manage_guild: 24 | return True 25 | return False 26 | 27 | async def check_is_mod(ctx): 28 | if await check_is_admin(ctx): 29 | return True 30 | if ctx.author.permissions_in(ctx.channel).manage_messages: 31 | return True 32 | return False 33 | 34 | def is_owner(): 35 | return commands.check(check_is_owner) 36 | 37 | def is_co_owner(): 38 | return commands.check(check_is_co_owner) 39 | 40 | def is_guildowner(): 41 | return commands.check(check_is_guildowner) 42 | 43 | def is_admin(): 44 | return commands.check(check_is_admin) 45 | 46 | def is_mod(): 47 | return commands.check(check_is_mod) 48 | 49 | def is_prefix(prefixes: list): 50 | def check(ctx): 51 | prefix = ctx.prefix 52 | return prefix in prefixes 53 | return commands.check(check) 54 | 55 | async def check_cog_enabled(ctx, default=True): 56 | enabled = await ctx.cog_enabled() 57 | return enabled if enabled is not None else default 58 | 59 | def cog_enabled(): 60 | return commands.check(check_cog_enabled) 61 | -------------------------------------------------------------------------------- /eevee/core/cog_base.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import logging 3 | 4 | from eevee.utils import Map 5 | 6 | 7 | class Cog: 8 | """Base Class for Cogs to inherit in order to automate Cog setup. 9 | 10 | Defined ``__new__`` method ensures the Cog class is initialised 11 | without explicitly calling ``super``. Works with multiple 12 | inheritances. 13 | 14 | Base __init__ automatically assigns ``self.bot`` attribute, sets up the 15 | logger while assigning it to ``self.logger`` attribute, and detects, 16 | creates if necessary and assigns the cog packages database tables to 17 | the ``self.tables`` attribute. 18 | """ 19 | 20 | _is_base = True 21 | 22 | def __new__(cls, bot, *args, **kwargs): 23 | instance = super().__new__(cls) 24 | def run_cog_init(klass): 25 | for base in klass.__bases__: 26 | if base is object: 27 | return 28 | elif base._is_base: 29 | base.__init__(instance, bot) 30 | else: 31 | run_cog_init(base) 32 | run_cog_init(instance.__class__) 33 | return instance 34 | 35 | def __init_subclass__(cls): 36 | cls._is_base = False 37 | 38 | def __init__(self, bot): 39 | self.bot = bot 40 | self.tables = None 41 | module = self.__class__.__module__ 42 | cog = self.__class__.__name__ 43 | log_name = f"{module}.{cog}" 44 | self.logger = logging.getLogger(log_name) 45 | self._check_tables_module() 46 | 47 | def _check_tables_module(self): 48 | this_module = self.__class__.__module__ 49 | try: 50 | tbl_mod = importlib.import_module('..tables', this_module) 51 | except ModuleNotFoundError: 52 | pass 53 | else: 54 | if not hasattr(tbl_mod, 'setup'): 55 | del tbl_mod 56 | else: 57 | self.bot.loop.create_task(self._table_setup(tbl_mod)) 58 | 59 | async def _table_setup(self, table_module): 60 | cog_name = self.__class__.__name__ 61 | cog_tables = table_module.setup(self.bot) 62 | if not isinstance(cog_tables, (list, tuple)): 63 | cog_tables = [cog_tables] 64 | self.tables = Map({t.name:t for t in cog_tables}) 65 | for table in self.tables.values(): 66 | if await table.exists(): 67 | self.logger.info( 68 | f'Cog table {table.name} for {cog_name} found.') 69 | table.new_columns = [] 70 | continue 71 | await table.create() 72 | self.logger.info(f'Cog table {table.name} for {cog_name} created.') 73 | del table_module 74 | -------------------------------------------------------------------------------- /eevee/core/cog_manager.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | 3 | from eevee.core import checks 4 | from eevee import command, group 5 | 6 | class CogManager: 7 | """Commands to add, remove and change cogs for Eevee.""" 8 | 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | def __local_check(self, ctx): 13 | return checks.check_is_co_owner(ctx) 14 | 15 | @property 16 | def available_exts(self): 17 | return [i[1] for i in pkgutil.iter_modules([self.bot.ext_dir])] 18 | 19 | async def load_extension(self, ctx, name, extension): 20 | ext_loc, ext = extension.rsplit('.', 1) 21 | if ext not in self.available_exts and ext_loc == "eevee.cogs": 22 | return await ctx.error(f'Extention {name} not found') 23 | was_loaded = extension in ctx.bot.extensions 24 | try: 25 | ctx.bot.unload_extension(extension) 26 | ctx.bot.load_extension(extension) 27 | except Exception as e: 28 | await ctx.error( 29 | f'Error when loading extension {name}', 30 | f'{type(e).__name__}: {e}', 31 | log_level='critical') 32 | else: 33 | await ctx.success( 34 | f"Extension {name} {'reloaded' if was_loaded else 'loaded'}") 35 | 36 | @group(category="Owner", aliases=['ext'], invoke_without_command=True) 37 | async def extension(self, ctx): 38 | """Commands to manage extensions.""" 39 | await ctx.bot.send_cmd_help(ctx) 40 | 41 | @extension.group(name="list", invoke_without_command=True) 42 | async def _list(self, ctx): 43 | """List all available extension modules and their status.""" 44 | all_exts = self.available_exts 45 | not_emoji = "\N{BLACK SMALL SQUARE}" 46 | is_emoji = "\N{WHITE SMALL SQUARE}" 47 | status_list = [] 48 | for ext in all_exts: 49 | is_loaded = f"eevee.cogs.{ext}" in ctx.bot.extensions 50 | status = is_emoji if is_loaded else not_emoji 51 | status_list.append(f"{status} {ext}") 52 | status_list.insert(0, f"{len(ctx.bot.extensions)} of {len(all_exts)}") 53 | await ctx.info('Available Extensions', '\n'.join(status_list)) 54 | 55 | @_list.command() 56 | async def full(self, ctx): 57 | await ctx.info('Full Extension List', '\n'.join(ctx.bot.extensions)) 58 | 59 | @extension.command(name='cogs') 60 | async def cogs(self, ctx): 61 | """List all loaded cogs.""" 62 | await ctx.info('Loaded Cogs', '\n'.join(ctx.bot.cogs)) 63 | 64 | @extension.command() 65 | async def unload(self, ctx, extension): 66 | """Unload an extension.""" 67 | ext_name = f"eevee.cogs.{extension}" 68 | if ext_name in ctx.bot.extensions: 69 | ctx.bot.unload_extension(ext_name) 70 | await ctx.success(f'Extension {extension} unloaded') 71 | else: 72 | await ctx.error(f"Extension {extension} isn't loaded") 73 | 74 | @extension.group(invoke_without_command=True) 75 | async def load(self, ctx, *extensions): 76 | """Load or reload an extension.""" 77 | if not extensions: 78 | return await ctx.bot.send_cmd_help(ctx) 79 | if len(extensions) > 5: 80 | return await ctx.warning( 81 | 'Please limit extensions for loading to 5 or less.') 82 | for ext in extensions: 83 | name = ext.replace('_', ' ').title() 84 | await self.load_extension(ctx, name, f"eevee.cogs.{ext}") 85 | 86 | @load.command(name="core") 87 | async def _core(self, ctx): 88 | """Reload Core Commands.""" 89 | await self.load_extension(ctx, 'Core Commands', 'eevee.core.commands') 90 | 91 | @load.command(name="cm") 92 | async def _cm(self, ctx): 93 | """Reload Cog Manager.""" 94 | await self.load_extension(ctx, 'Cog Manager', 'eevee.core.cog_manager') 95 | 96 | @command(category='Owner', name='reload', aliases=['load']) 97 | async def _reload(self, ctx, *, cogs): 98 | """Reload Cog""" 99 | ctx.message.content = f"{ctx.prefix}ext load {cogs}" 100 | await ctx.bot.process_commands(ctx.message) 101 | 102 | def setup(bot): 103 | bot.add_cog(CogManager(bot)) 104 | -------------------------------------------------------------------------------- /eevee/core/data_manager/__init__.py: -------------------------------------------------------------------------------- 1 | from .manager import DataManager 2 | from .dbi import DatabaseInterface 3 | from .schema import Table 4 | from .errors import SchemaError 5 | from .tables import CogTable 6 | -------------------------------------------------------------------------------- /eevee/core/data_manager/dbi.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | 4 | import asyncpg 5 | 6 | from discord.ext.commands import when_mentioned_or 7 | 8 | from .schema import Table, Query, Insert, Update, Schema 9 | from .tables import core_table_sqls 10 | from . import sqltypes 11 | 12 | logger = logging.getLogger('eevee.dbi') 13 | 14 | async def init_conn(conn): 15 | await conn.set_type_codec( 16 | "jsonb", encoder=json.dumps, decoder=json.loads, schema="pg_catalog") 17 | await conn.set_type_codec( 18 | "json", encoder=json.dumps, decoder=json.loads, schema="pg_catalog") 19 | 20 | class DatabaseInterface: 21 | """Get, Create and Edit data in the connected database.""" 22 | 23 | def __init__(self, 24 | password, 25 | hostname='localhost', 26 | username='eevee', 27 | database="eevee", 28 | port=5432): 29 | self.loop = None 30 | self.dsn = "postgres://{}:{}@{}:{}/{}".format( 31 | username, password, hostname, port, database) 32 | self.pool = None 33 | self.settings_conn = None 34 | self.settings_stmt = None 35 | self.types = sqltypes 36 | 37 | async def start(self, loop=None): 38 | if loop: 39 | self.loop = loop 40 | self.pool = await asyncpg.create_pool( 41 | self.dsn, loop=loop, init=init_conn) 42 | await self.prepare() 43 | 44 | async def recreate_pool(self): 45 | logger.warning(f'Re-creating closed database pool.') 46 | self.pool = await asyncpg.create_pool( 47 | self.dsn, loop=self.loop, init=init_conn) 48 | 49 | async def prepare(self): 50 | # ensure tables exists 51 | await self.core_tables_exist() 52 | 53 | # guild settings statement 54 | self.settings_conn = await self.pool.acquire() 55 | settings_sql = ('SELECT config_value FROM guild_config ' 56 | 'WHERE guild_id=$1 AND config_name=$2;') 57 | self.settings_stmt = await self.settings_conn.prepare(settings_sql) 58 | 59 | async def core_tables_exist(self): 60 | core_sql = core_table_sqls() 61 | for k, v in core_sql.items(): 62 | table_exists = await self.table(k).exists() 63 | if not table_exists: 64 | logger.warning(f'Core table {k} not found. Creating...') 65 | await self.execute_transaction(v) 66 | logger.warning(f'Core table {k} created.') 67 | 68 | async def stop(self): 69 | conns = (self.settings_conn,) 70 | for c in conns: 71 | if c: 72 | await self.pool.release(c) 73 | if self.pool: 74 | await self.pool.close() 75 | self.pool.terminate() 76 | 77 | async def execute_query(self, query, *query_args): 78 | result = [] 79 | try: 80 | async with self.pool.acquire() as conn: 81 | stmt = await conn.prepare(query) 82 | rcrds = await stmt.fetch(*query_args) 83 | for rcrd in rcrds: 84 | result.append(rcrd) 85 | return result 86 | except asyncpg.exceptions.InterfaceError as e: 87 | logger.error(f'Exception {type(e)}: {e}') 88 | await self.recreate_pool() 89 | return await self.execute_query(query, *query_args) 90 | 91 | async def execute_transaction(self, query, *query_args): 92 | result = [] 93 | try: 94 | async with self.pool.acquire() as conn: 95 | stmt = await conn.prepare(query) 96 | 97 | if any(isinstance(x, (set, tuple)) for x in query_args): 98 | async with conn.transaction(): 99 | for query_arg in query_args: 100 | async for rcrd in stmt.cursor(*query_arg): 101 | result.append(rcrd) 102 | else: 103 | async with conn.transaction(): 104 | async for rcrd in stmt.cursor(*query_args): 105 | result.append(rcrd) 106 | return result 107 | except asyncpg.exceptions.InterfaceError: 108 | await self.recreate_pool() 109 | return await self.execute_transaction(query, *query_args) 110 | 111 | async def create_table(self, name, columns: list, *, primaries=None): 112 | """Create table.""" 113 | return await Table(self, name).create(columns, primaries=primaries) 114 | 115 | def table(self, name): 116 | return Table(name, self) 117 | 118 | def query(self, *tables): 119 | return Query(self, *tables) 120 | 121 | def insert(self, table): 122 | return Insert(self, table) 123 | 124 | def update(self, table): 125 | return Update(self, table) 126 | 127 | async def tables(self): 128 | table = self.table('information_schema.tables') 129 | table.query('table_name') 130 | table.query.where(table_schema='public') 131 | table.query.order_by('table_name') 132 | return await table.query.get() 133 | 134 | def schema(self, name): 135 | return Schema(self, name) 136 | -------------------------------------------------------------------------------- /eevee/core/data_manager/errors.py: -------------------------------------------------------------------------------- 1 | from asyncpg import PostgresError 2 | 3 | class SchemaError(PostgresError): 4 | pass 5 | 6 | class ResponseError(PostgresError): 7 | pass 8 | 9 | class QueryError(PostgresError): 10 | pass 11 | -------------------------------------------------------------------------------- /eevee/core/data_manager/guild.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | class GuildDM: 4 | """Manage guild data/settings.""" 5 | 6 | def __init__(self, dbi, guild): 7 | self.dbi = dbi 8 | if isinstance(guild, discord.Guild): 9 | guild = guild.id 10 | self.guild_id = int(guild) 11 | 12 | async def settings(self, key=None, value=None, *, delete=False): 13 | config_table = self.dbi.table('guild_config') 14 | if delete: 15 | if key: 16 | return await config_table.delete( 17 | guild_id=self.guild_id, config_name=str(key)) 18 | else: 19 | return None 20 | if key is not None: 21 | if value is not None: 22 | return await config_table.upsert( 23 | guild_id=self.guild_id, 24 | config_name=str(key), config_value=str(value)) 25 | else: 26 | return await self.dbi.settings_stmt.fetchval( 27 | self.guild_id, str(key)) 28 | else: 29 | return await config_table.get(guild_id=self.guild_id) 30 | 31 | async def prefix(self, new_prefix: str = None): 32 | """Add, remove and change custom guild prefix. 33 | 34 | Get current prefix by calling without args. 35 | Set new prefix by calling with the new prefix as an arg. 36 | Reset prefix to default by calling 'reset' as an arg. 37 | """ 38 | pfx_tbl = self.dbi.table('prefix') 39 | pfx_tbl.query.where(guild_id=self.guild_id) 40 | if new_prefix: 41 | if new_prefix.lower() == "reset": 42 | return await pfx_tbl.query.delete() 43 | pfx_tbl.insert(guild_id=self.guild_id, prefix=new_prefix) 44 | pfx_tbl.insert.primaries('guild_id') 45 | return await pfx_tbl.insert.commit(do_update=True) 46 | else: 47 | return await pfx_tbl.query.get_value('prefix') 48 | -------------------------------------------------------------------------------- /eevee/core/data_manager/manager.py: -------------------------------------------------------------------------------- 1 | from .guild import GuildDM 2 | 3 | class DataManager: 4 | """Query and data handling""" 5 | 6 | def __init__(self, db): 7 | self._db = db 8 | 9 | def guild(self, guild_id): 10 | """Guild Data Manager""" 11 | return GuildDM(self._db, guild_id) 12 | -------------------------------------------------------------------------------- /eevee/core/data_manager/sqltypes.py: -------------------------------------------------------------------------------- 1 | import pydoc 2 | import datetime 3 | import decimal 4 | from .errors import SchemaError 5 | 6 | 7 | class SQLType: 8 | python = None 9 | 10 | def to_dict(self): 11 | dic = self.__dict__.copy() 12 | cls = self.__class__ 13 | dic['__meta__'] = cls.__module__ + '.' + cls.__qualname__ 14 | return dic 15 | 16 | @classmethod 17 | def from_dict(cls, data): 18 | meta = data.pop('__meta__') 19 | given = cls.__module__ + '.' + cls.__qualname__ 20 | if given != meta: 21 | cls = pydoc.locate(meta) 22 | if cls is None: 23 | raise RuntimeError(f'Could not locate "{meta}".') 24 | self = cls.__new__(cls) 25 | self.__dict__.update(data) 26 | return self 27 | 28 | def __eq__(self, other): 29 | return isinstance( 30 | other, self.__class__) and self.__dict__ == other.__dict__ 31 | 32 | def __ne__(self, other): 33 | return not self.__eq__(other) 34 | 35 | def to_sql(self): 36 | raise NotImplementedError() 37 | 38 | def is_real_type(self): 39 | return True 40 | 41 | 42 | class BooleanSQL(SQLType): 43 | python = bool 44 | 45 | def to_sql(self): 46 | return 'BOOLEAN' 47 | 48 | 49 | class DateSQL(SQLType): 50 | python = datetime.date 51 | 52 | def to_sql(self): 53 | return 'DATE' 54 | 55 | 56 | class DatetimeSQL(SQLType): 57 | python = datetime.datetime 58 | 59 | def __init__(self, *, timezone=False): 60 | self.timezone = timezone 61 | 62 | def to_sql(self): 63 | if self.timezone: 64 | return 'TIMESTAMP WITH TIMEZONE' 65 | return 'TIMESTAMP' 66 | 67 | 68 | class DoubleSQL(SQLType): 69 | python = float 70 | 71 | def to_sql(self): 72 | return 'REAL' 73 | 74 | 75 | class FloatSQL(SQLType): 76 | python = float 77 | 78 | def to_sql(self): 79 | return 'FLOAT' 80 | 81 | 82 | class IntegerSQL(SQLType): 83 | python = int 84 | 85 | def __init__(self, *, big=False, small=False, auto_increment=False): 86 | self.big = big 87 | self.small = small 88 | self.auto_increment = auto_increment 89 | 90 | if big and small: 91 | raise SchemaError('Integer column type cannot be both big and small.') 92 | 93 | def to_sql(self): 94 | if self.auto_increment: 95 | if self.big: 96 | return 'BIGSERIAL' 97 | if self.small: 98 | return 'SMALLSERIAL' 99 | return 'SERIAL' 100 | if self.big: 101 | return 'BIGINT' 102 | if self.small: 103 | return 'SMALLINT' 104 | return 'INTEGER' 105 | 106 | def is_real_type(self): 107 | return not self.auto_increment 108 | 109 | 110 | class IntervalSQL(SQLType): 111 | python = datetime.timedelta 112 | valid_fields = ( 113 | 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND', 'YEAR TO MONTH', 114 | 'DAY TO HOUR', 'DAY TO MINUTE', 'DAY TO SECOND', 'HOUR TO MINUTE', 115 | 'HOUR TO SECOND', 'MINUTE TO SECOND') 116 | 117 | def __init__(self, field=None): 118 | if field: 119 | field = field.upper() 120 | if field not in self.valid_fields: 121 | raise SchemaError('invalid interval specified') 122 | self.field = field 123 | else: 124 | self.field = None 125 | 126 | def to_sql(self): 127 | if self.field: 128 | return 'INTERVAL ' + self.field 129 | return 'INTERVAL' 130 | 131 | 132 | class DecimalSQL(SQLType): 133 | python = decimal.Decimal 134 | 135 | def __init__(self, *, precision=None, scale=None): 136 | if precision is not None: 137 | if precision < 0 or precision > 1000: 138 | raise SchemaError( 139 | 'precision must be greater than 0 and below 1000') 140 | if scale is None: 141 | scale = 0 142 | 143 | self.precision = precision 144 | self.scale = scale 145 | 146 | def to_sql(self): 147 | if self.precision is not None: 148 | return f'NUMERIC({self.precision}, {self.scale})' 149 | return 'NUMERIC' 150 | 151 | 152 | class StringSQL(SQLType): 153 | python = str 154 | 155 | def to_sql(self): 156 | return 'TEXT' 157 | 158 | 159 | class TimeSQL(SQLType): 160 | python = datetime.time 161 | 162 | def __init__(self, *, timezone=False): 163 | self.timezone = timezone 164 | 165 | def to_sql(self): 166 | if self.timezone: 167 | return 'TIME WITH TIME ZONE' 168 | return 'TIME' 169 | 170 | 171 | class JSONSQL(SQLType): 172 | python = None 173 | 174 | def to_sql(self): 175 | return 'JSONB' 176 | 177 | class ArraySQL(SQLType): 178 | def __init__(self, inner_type, size: int = None): 179 | if not isinstance(inner_type, SQLType): 180 | raise SchemaError('Array inner type must be an SQLType') 181 | self.type = inner_type 182 | self.size = size 183 | 184 | def to_sql(self): 185 | if self.size: 186 | return f"{self.type.to_sql()}[{self.size}]" 187 | return f"{self.type.to_sql()}[]" 188 | -------------------------------------------------------------------------------- /eevee/core/data_manager/tables.py: -------------------------------------------------------------------------------- 1 | from eevee.core.data_manager import schema 2 | from eevee.core.logger import LOGGERS 3 | 4 | def core_table_sqls(): 5 | sql_dict = { 6 | 'guild_config' : ("CREATE TABLE guild_config (" 7 | "guild_id bigint NOT NULL, " 8 | "config_name text NOT NULL, " 9 | "config_value text NOT NULL, " 10 | "CONSTRAINT guild_config_pk " 11 | "PRIMARY KEY (guild_id, config_name));"), 12 | 13 | 'prefix' : ("CREATE TABLE prefix (" 14 | "guild_id bigint NOT NULL, " 15 | "prefix text NOT NULL, " 16 | "CONSTRAINT prefixes_pkey " 17 | "PRIMARY KEY (guild_id));"), 18 | 19 | 'discord_messages' : ("CREATE TABLE discord_messages (" 20 | "message_id bigint NOT NULL, " 21 | "sent bigint NOT NULL, " 22 | "is_edit bool NOT NULL DEFAULT FALSE, " 23 | "deleted bool NOT NULL DEFAULT FALSE, " 24 | "author_id bigint NOT NULL, " 25 | "channel_id bigint NOT NULL, " 26 | "guild_id bigint, " 27 | "content text, " 28 | "clean_content text, " 29 | "embeds jsonb[], " 30 | "webhook_id bigint, " 31 | "attachments text[], " 32 | "CONSTRAINT discord_messages_pkey " 33 | "PRIMARY KEY (message_id, sent));"), 34 | 35 | 'member_activity' : ("CREATE TABLE member_activity (" 36 | "member_id bigint NOT NULL, " 37 | "time bigint NOT NULL, " 38 | "status text, " 39 | "from_status text, " 40 | "guild_id bigint, " 41 | "display_name text, " 42 | "CONSTRAINT member_activity_pkey " 43 | "PRIMARY KEY (member_id, time));"), 44 | 45 | 'command_log' : ("CREATE TABLE command_log (" 46 | "message_id bigint NOT NULL, " 47 | "sent bigint NOT NULL, " 48 | "author_id bigint NOT NULL, " 49 | "channel_id bigint NOT NULL, " 50 | "guild_id bigint, " 51 | "prefix text NOT NULL, " 52 | "command text NOT NULL, " 53 | "invoked_with text NOT NULL, " 54 | "invoked_subcommand text, " 55 | "subcommand_passed text, " 56 | "command_failed bool NOT NULL DEFAULT FALSE, " 57 | "cog text, " 58 | "CONSTRAINT command_log_pkey " 59 | "PRIMARY KEY (message_id, sent));") 60 | } 61 | 62 | log_sql = ("CREATE TABLE {log_table} (" 63 | "log_id bigint NOT NULL, " 64 | "created bigint NOT NULL, " 65 | "logger_name text, " 66 | "level_name text, " 67 | "file_path text, " 68 | "module text, " 69 | "func_name text, " 70 | "line_no int, " 71 | "message text, " 72 | "traceback text, " 73 | "CONSTRAINT {log_table}_pkey " 74 | "PRIMARY KEY (log_id));") 75 | 76 | for log in LOGGERS: 77 | sql_dict[log] = log_sql.format(log_table=log) 78 | 79 | return sql_dict 80 | 81 | 82 | class CogTable: 83 | table_config = { 84 | "name" : "base_default_table", 85 | "columns" : { 86 | "id" : {"cls" : schema.IDColumn}, 87 | "value" : {"cls" : schema.StringColumn} 88 | }, 89 | "primaries" : ("id") 90 | } 91 | 92 | def __init__(self, bot): 93 | self.dbi = bot.dbi 94 | self.bot = bot 95 | 96 | def convert_columns(self, columns_dict=None): 97 | columns = [] 98 | for k, v in columns_dict.items(): 99 | col_cls = v.pop('cls', schema.Column) 100 | columns.append(col_cls(k, **v)) 101 | return columns 102 | 103 | async def setup(self, table_name=None, columns: list = None, *, primaries=None): 104 | table_name = table_name or self.table_config["name"] 105 | table = self.dbi.table(table_name) 106 | exists = table.exists() 107 | if not exists: 108 | columns = self.convert_columns(self.table_config["columns"]) 109 | primaries = primaries or self.table_config["primaries"] 110 | table = await table.create( 111 | self.dbi, table_name, columns, primaries=primaries) 112 | return table 113 | -------------------------------------------------------------------------------- /eevee/core/error_handling.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | from discord.ext import commands 4 | 5 | from eevee import errors 6 | 7 | class ErrorHandler: 8 | 9 | async def on_command_error(self, ctx, error): 10 | if isinstance(error, commands.MissingRequiredArgument): 11 | await ctx.bot.send_cmd_help( 12 | ctx, title='Missing Arguments', msg_type='error') 13 | elif isinstance(error, commands.BadArgument): 14 | await ctx.bot.send_cmd_help( 15 | ctx, title=f'Bad Argument - {error}', msg_type='error') 16 | elif isinstance(error, errors.MissingSubcommand): 17 | await ctx.bot.send_cmd_help( 18 | ctx, title=f'Missing Subcommand - {error}', msg_type='error') 19 | elif isinstance(error, commands.DisabledCommand): 20 | await ctx.send("That command is disabled.") 21 | elif isinstance(error, commands.CommandInvokeError): 22 | ctx.bot.logger.exception( 23 | "Exception in command '{}'".format(ctx.command.qualified_name), 24 | exc_info=error.original) 25 | message = ("Error in command '{}'. Check your console or " 26 | "logs for details." 27 | "".format(ctx.command.qualified_name)) 28 | exception_log = ("Exception in command '{}'\n" 29 | "".format(ctx.command.qualified_name)) 30 | exception_log += "".join(traceback.format_exception( 31 | type(error), error, error.__traceback__)) 32 | ctx.bot._last_exception = exception_log 33 | await ctx.send(message) 34 | 35 | elif isinstance(error, commands.CommandNotFound): 36 | pass 37 | elif isinstance(error, commands.CheckFailure): 38 | pass 39 | elif isinstance(error, commands.NoPrivateMessage): 40 | await ctx.send("That command is not available in DMs.") 41 | elif isinstance(error, commands.CommandOnCooldown): 42 | await ctx.send("This command is on cooldown. " 43 | "Try again in {:.2f}s" 44 | "".format(error.retry_after)) 45 | else: 46 | ctx.bot.logger.exception(type(error).__name__, exc_info=error) 47 | 48 | def setup(bot): 49 | bot.add_cog(ErrorHandler()) 50 | -------------------------------------------------------------------------------- /eevee/core/errors.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | 3 | class MissingSubcommand(commands.CommandError): 4 | pass 5 | -------------------------------------------------------------------------------- /eevee/core/logger.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import os 4 | import sys 5 | import time 6 | import traceback 7 | import logging 8 | from logging import handlers 9 | from datetime import timezone 10 | 11 | import asyncpg 12 | import discord 13 | 14 | from eevee.utils import snowflake 15 | 16 | get_id = snowflake.create() 17 | 18 | LOGGERS = ('eevee_logs', 'discord_logs') 19 | 20 | module_logger = logging.getLogger('eevee.core.logger') 21 | 22 | def init_logger(bot, debug_flag=False): 23 | 24 | # set root logger level 25 | logging.getLogger().setLevel(logging.DEBUG) 26 | 27 | # setup discord logger 28 | discord_log = logging.getLogger("discord") 29 | discord_log.setLevel(logging.INFO) 30 | 31 | # setup eevee logger 32 | eevee_log = logging.getLogger("eevee") 33 | 34 | # setup log directory 35 | log_path = os.path.join(bot.data_dir, 'logs') 36 | if not os.path.exists(log_path): 37 | os.makedirs(log_path) 38 | 39 | # file handler factory 40 | def create_fh(file_name): 41 | fh_path = os.path.join(log_path, file_name) 42 | return handlers.RotatingFileHandler( 43 | filename=fh_path, encoding='utf-8', mode='a', 44 | maxBytes=400000, backupCount=20) 45 | 46 | # set eevee log formatting 47 | log_format = logging.Formatter( 48 | '%(asctime)s %(name)s %(levelname)s %(module)s %(funcName)s %(lineno)d: ' 49 | '%(message)s', 50 | datefmt="[%d/%m/%Y %H:%M]") 51 | 52 | # create file handlers 53 | eevee_fh = create_fh('eevee.log') 54 | eevee_fh.setLevel(logging.INFO) 55 | eevee_fh.setFormatter(log_format) 56 | eevee_log.addHandler(eevee_fh) 57 | discord_fh = create_fh('discord.log') 58 | discord_fh.setLevel(logging.INFO) 59 | discord_fh.setFormatter(log_format) 60 | discord_log.addHandler(discord_fh) 61 | 62 | # create console handler 63 | console_std = sys.stdout if debug_flag else sys.stderr 64 | eevee_console = logging.StreamHandler(console_std) 65 | eevee_console.setLevel(logging.INFO if debug_flag else logging.ERROR) 66 | eevee_console.setFormatter(log_format) 67 | eevee_log.addHandler(eevee_console) 68 | discord_console = logging.StreamHandler(console_std) 69 | discord_console.setLevel(logging.ERROR) 70 | discord_console.setFormatter(log_format) 71 | discord_log.addHandler(discord_console) 72 | 73 | # create db handler 74 | eevee_db = DBLogHandler(bot, 'eevee_logs') 75 | bot.log_eevee_db = eevee_db 76 | eevee_log.addHandler(eevee_db) 77 | discord_db = DBLogHandler(bot, 'discord_logs') 78 | discord_log.addHandler(discord_db) 79 | 80 | bot.add_cog(ActivityLogging(bot)) 81 | 82 | return eevee_log 83 | 84 | class DBLogHandler(logging.Handler): 85 | def __init__(self, bot, log_name: str, level=logging.INFO): 86 | if log_name not in LOGGERS: 87 | raise RuntimeError(f'Unknown Log Name: {log_name}') 88 | self.bot = bot 89 | self.log_name = log_name 90 | self.logger = module_logger.getChild('DBLogHandler') 91 | super().__init__(level=level) 92 | 93 | def emit(self, record): 94 | record_id = next(get_id) 95 | asyncio.run_coroutine_threadsafe( 96 | self.submit_log(record_id, record), self.bot.loop) 97 | 98 | async def submit_log(self, log_id, record): 99 | data = dict(log_id=log_id, 100 | created=record.created, 101 | logger_name=str(record.name), 102 | level_name=str(record.levelname), 103 | file_path=str(record.pathname), 104 | module=str(record.module), 105 | func_name=str(record.funcName), 106 | line_no=record.lineno, 107 | message=str(record.message), 108 | traceback=''.join( 109 | traceback.format_exception(*record.exc_info))) 110 | try: 111 | table = self.bot.dbi.table(self.log_name) 112 | table.insert(**data) 113 | await table.insert.commit() 114 | except asyncpg.PostgresError as e: 115 | self.logger.exception(type(e).__name__, exc_info=e) 116 | 117 | class ActivityLogging: 118 | def __init__(self, bot): 119 | self.bot = bot 120 | self.logger = module_logger.getChild('ActivityLogging') 121 | 122 | async def on_message(self, msg): 123 | sent = int(msg.created_at.replace(tzinfo=timezone.utc).timestamp()) 124 | guild_id = msg.guild.id if msg.guild else None 125 | embeds = [json.dumps(e.to_dict()) for e in msg.embeds] 126 | attachments = [a.url for a in msg.attachments] 127 | data = dict(message_id=msg.id, sent=sent, is_edit=False, deleted=False, 128 | author_id=msg.author.id, channel_id=msg.channel.id, 129 | guild_id=guild_id, content=msg.content, 130 | clean_content=msg.clean_content, embeds=embeds, 131 | webhook_id=msg.webhook_id, attachments=attachments) 132 | try: 133 | table = self.bot.dbi.table('discord_messages') 134 | table.insert(**data) 135 | await table.insert.commit() 136 | except asyncpg.PostgresError as e: 137 | self.logger.exception(type(e).__name__, exc_info=e) 138 | 139 | async def on_raw_message_delete(self, payload): 140 | try: 141 | table = self.bot.dbi.table('discord_messages') 142 | table.update(deleted=True) 143 | table.update.where(message_id=payload.message_id) 144 | await table.update.commit() 145 | except asyncpg.PostgresError as e: 146 | self.logger.exception(type(e).__name__, exc_info=e) 147 | 148 | async def on_raw_bulk_message_delete(self, payload): 149 | try: 150 | table = self.bot.dbi.table('discord_messages') 151 | table.update(deleted=True) 152 | m_ids = payload.message_ids 153 | conditions = [table['message_id'] == m_id for m_id in m_ids] 154 | table.update.where(conditions) 155 | await table.update.commit() 156 | except asyncpg.PostgresError as e: 157 | self.logger.exception(type(e).__name__, exc_info=e) 158 | 159 | async def on_message_edit(self, before, after): 160 | if before.type == discord.MessageType.call: 161 | return 162 | if before.content == after.content: 163 | return 164 | msg = after 165 | sent = int(msg.edited_at.replace(tzinfo=timezone.utc).timestamp()) 166 | guild_id = msg.guild.id if msg.guild else None 167 | embeds = [json.dumps(e.to_dict()) for e in msg.embeds] 168 | attachments = [a.url for a in msg.attachments] 169 | data = dict(message_id=msg.id, sent=sent, is_edit=True, deleted=False, 170 | author_id=msg.author.id, channel_id=msg.channel.id, 171 | guild_id=guild_id, content=msg.content, 172 | clean_content=msg.clean_content, embeds=embeds, 173 | webhook_id=msg.webhook_id, attachments=attachments) 174 | try: 175 | table = self.bot.dbi.table('discord_messages') 176 | table.insert(**data) 177 | # update existing data 178 | table.insert.primaries('message_id', 'sent') 179 | await table.insert.commit(do_update=True) 180 | except asyncpg.PostgresError as e: 181 | self.logger.exception(type(e).__name__, exc_info=e) 182 | 183 | async def on_command(self, ctx): 184 | created = ctx.message.created_at 185 | sent = int(created.replace(tzinfo=timezone.utc).timestamp()) 186 | guild = ctx.guild.id if ctx.guild else None 187 | cog = ctx.cog.__class__.__name__ if ctx.cog else None 188 | isc = ctx.invoked_subcommand.name if ctx.invoked_subcommand else None 189 | cmd = ctx.command.name if ctx.command else None 190 | data = dict(message_id=ctx.message.id, sent=sent, 191 | author_id=ctx.author.id, channel_id=ctx.channel.id, 192 | guild_id=guild, prefix=ctx.prefix, 193 | command=cmd, invoked_with=ctx.invoked_with, 194 | invoked_subcommand=isc, 195 | subcommand_passed=ctx.subcommand_passed, 196 | command_failed=ctx.command_failed, cog=cog) 197 | try: 198 | table = self.bot.dbi.table('command_log') 199 | table.insert(**data) 200 | table.insert.primaries('message_id', 'sent') 201 | # ignore conflicts 202 | await table.insert.commit(do_update=False) 203 | except asyncpg.PostgresError as e: 204 | self.logger.exception(type(e).__name__, exc_info=e) 205 | 206 | async def on_member_update(self, before, after): 207 | status_update = None 208 | status_from = None 209 | name_update = None 210 | 211 | if before.status != after.status: 212 | status_update = str(after.status) 213 | status_from = str(before.status) 214 | 215 | if before.nick != after.nick and after.nick: 216 | name_update = after.nick 217 | 218 | if not status_update and not name_update: 219 | return 220 | 221 | time_value = int(time.time()) 222 | guild = after.guild.id if after.guild else None 223 | 224 | data = dict(member_id=after.id, time=time_value, 225 | status=status_update, from_status=status_from, 226 | guild_id=guild, display_name=name_update) 227 | 228 | try: 229 | table = self.bot.dbi.table('member_activity') 230 | table.insert(**data) 231 | table.insert.primaries('member_id', 'time') 232 | # ignore conflicts 233 | await table.insert.commit(do_update=False) 234 | except asyncpg.PostgresError as e: 235 | self.logger.exception(type(e).__name__, exc_info=e) 236 | -------------------------------------------------------------------------------- /eevee/data/permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "kick_members":1, 3 | "ban_members":2, 4 | "administrator":3, 5 | "manage_channels":4, 6 | "manage_guild":5, 7 | "add_reactions":6, 8 | "view_audit_log":7, 9 | "read_messages":10, 10 | "send_messages":11, 11 | "send_tts_messages":12, 12 | "manage_messages":13, 13 | "embed_links":14, 14 | "attach_files":15, 15 | "read_message_history":16, 16 | "mention_everyone":17, 17 | "external_emojis":18, 18 | "connect":20, 19 | "speak":21, 20 | "mute_members":22, 21 | "deafen_members":23, 22 | "move_members":24, 23 | "use_voice_activation":25, 24 | "change_nickname":26, 25 | "manage_nicknames":27, 26 | "manage_roles":28, 27 | "manage_webhooks":29, 28 | "manage_emojis":30 29 | } 30 | -------------------------------------------------------------------------------- /eevee/data/pokedex-temp.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scragly/Eevee/b71ec4ac74dc29a6774cc377fbde3d02d1e5a5c0/eevee/data/pokedex-temp.sqlite -------------------------------------------------------------------------------- /eevee/data/raid_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "raid_pkmn" : { 3 | "metapod" : { 4 | "level" : 1, 5 | "max_cp" : 239, 6 | "max_cp_w": 299 7 | }, 8 | "wailmer" : { 9 | "level" : 1, 10 | "max_cp" : 814, 11 | "max_cp_w": 1017 12 | }, 13 | "ivysaur" : { 14 | "level" : 1, 15 | "max_cp" : 886, 16 | "max_cp_w": 1108 17 | }, 18 | "charmeleon" : { 19 | "level" : 1, 20 | "max_cp" : 847, 21 | "max_cp_w": 1060 22 | }, 23 | "wartortle" : { 24 | "level" : 1, 25 | "max_cp" : 756, 26 | "max_cp_w": 945 27 | }, 28 | "sableye" : { 29 | "level" : 1, 30 | "max_cp" : 745, 31 | "max_cp_w": 932 32 | }, 33 | "marowak" :{ 34 | "level" : 2, 35 | "max_cp" : 966, 36 | "max_cp_w": 1208 37 | }, 38 | "tentacruel" :{ 39 | "level" : 2, 40 | "max_cp" : 1356, 41 | "max_cp_w": 1695 42 | }, 43 | "sandslash" :{ 44 | "level" : 2, 45 | "max_cp" : 1330, 46 | "max_cp_w": 1663 47 | }, 48 | "magneton" :{ 49 | "level" : 2, 50 | "max_cp" : 1278, 51 | "max_cp_w": 1598 52 | }, 53 | "mawile" :{ 54 | "level" : 2, 55 | "max_cp" : 848, 56 | "max_cp_w": 1060 57 | }, 58 | "cloyster" :{ 59 | "level" : 2, 60 | "max_cp" : 1414, 61 | "max_cp_w": 1767 62 | }, 63 | "machamp" :{ 64 | "level" : 3, 65 | "max_cp" : 1650, 66 | "max_cp_w": 2063 67 | }, 68 | "alakazam" :{ 69 | "level" : 3, 70 | "max_cp" : 1649, 71 | "max_cp_w": 2062 72 | }, 73 | "gengar" :{ 74 | "level" : 3, 75 | "max_cp" : 1496, 76 | "max_cp_w": 1870 77 | }, 78 | "ninetales" :{ 79 | "level" : 3, 80 | "max_cp" : 1233, 81 | "max_cp_w": 1541 82 | }, 83 | "scyther" :{ 84 | "level" : 3, 85 | "max_cp" : 1408, 86 | "max_cp_w": 1760 87 | }, 88 | "omastar" :{ 89 | "level" : 3, 90 | "max_cp" : 1534, 91 | "max_cp_w": 1918 92 | }, 93 | "porygon" :{ 94 | "level" : 3, 95 | "max_cp" : 895, 96 | "max_cp_w": 1120 97 | }, 98 | "lapras" :{ 99 | "level" : 4, 100 | "max_cp" : 1487, 101 | "max_cp_w": 1859 102 | }, 103 | "rhydon" :{ 104 | "level" : 4, 105 | "max_cp" : 1886, 106 | "max_cp_w": 2359 107 | }, 108 | "tyranitar" :{ 109 | "level" : 4, 110 | "max_cp" : 2097, 111 | "max_cp_w": 2621 112 | }, 113 | "poliwrath" :{ 114 | "level" : 4, 115 | "max_cp" : 1395, 116 | "max_cp_w": 1744 117 | }, 118 | "victreebel" :{ 119 | "level" : 4, 120 | "max_cp" : 1296, 121 | "max_cp_w": 1620 122 | }, 123 | "golem" :{ 124 | "level" : 4, 125 | "max_cp" : 1666, 126 | "max_cp_w": 2083 127 | }, 128 | "nidoking" :{ 129 | "level" : 4, 130 | "max_cp" : 1363, 131 | "max_cp_w": 1704 132 | }, 133 | "nidoqueen" :{ 134 | "level" : 4, 135 | "max_cp" : 1336, 136 | "max_cp_w": 1670 137 | }, 138 | "absol" :{ 139 | "level" : 4, 140 | "max_cp" : 1303, 141 | "max_cp_w": 1629 142 | }, 143 | "groudon" :{ 144 | "level" : 5, 145 | "max_cp" : 2328, 146 | "max_cp_w": 2910 147 | }, 148 | "kyogre" :{ 149 | "level" : 5, 150 | "max_cp" : 2328, 151 | "max_cp_w": 2910 152 | }, 153 | "mewtwo" : { 154 | "level" : 5, 155 | "max_cp" : 2275, 156 | "max_cp_w": 2844, 157 | "exraid" : true 158 | } 159 | }, 160 | "raid_eggs": { 161 | "1": { 162 | "img_url": "https://i.imgur.com/31QYDXd.png" 163 | }, 164 | "2": { 165 | "img_url": "https://i.imgur.com/31QYDXd.png" 166 | }, 167 | "3": { 168 | "img_url": "https://i.imgur.com/yfcjQTj.png" 169 | }, 170 | "4": { 171 | "img_url": "https://i.imgur.com/yfcjQTj.png" 172 | }, 173 | "5": { 174 | "img_url": "https://i.imgur.com/CciZWAa.png" 175 | } 176 | } 177 | } 178 | 179 | -------------------------------------------------------------------------------- /eevee/launcher.py: -------------------------------------------------------------------------------- 1 | """Eevee launcher with auto-restart ability. 2 | 3 | Command: 4 | ``eevee`` 5 | 6 | Options: 7 | -r, --no-restart Disable auto-restart. 8 | --debug Enable debug mode. 9 | 10 | Exit codes: 11 | ===== ======== =========== 12 | Value Name Description 13 | ===== ======== =========== 14 | 0 SHUTDOWN Close down laucher. 15 | 1 CRITICAL Inform of crash and attempt restart by default. 16 | 26 RESTART Restart Meowth. 17 | ===== ======== =========== 18 | """ 19 | 20 | import argparse 21 | import subprocess 22 | import sys 23 | import time 24 | 25 | def parse_cli_args(): 26 | 27 | # create parser 28 | parser = argparse.ArgumentParser( 29 | description="Eevee - Pokemon Go Bot for Discord") 30 | 31 | # restart disable option 32 | parser.add_argument( 33 | "--no-restart", "-r", 34 | help="Disables auto-restart.", action="store_true") 35 | 36 | # debug enable option 37 | parser.add_argument( 38 | "--debug", "-d", help="Enabled debug mode.", action="store_true") 39 | 40 | # parse and return all provided args 41 | return parser.parse_known_args() 42 | 43 | def main(): 44 | 45 | # show intro banner 46 | print("==================================\n" 47 | "Eevee - Pokemon Go Bot for Discord\n" 48 | "==================================\n") 49 | 50 | # check if Python version is within requirements 51 | if sys.version_info < (3, 6, 1): 52 | print("ERROR: Minimum Python version not met.\n" 53 | "Eevee requires Python 3.6.1 or higher.\n") 54 | return 55 | 56 | # parse arguments provided and sort between launcher and unknown args 57 | launch_args, eevee_args = parse_cli_args() 58 | 59 | # forward the debug flag to the bot args 60 | if launch_args.debug: 61 | eevee_args.append('-d') 62 | 63 | # add the launcher flag to the bot args so it enabled restarting 64 | eevee_args.append('-l') 65 | 66 | # start tracking how many retries occurred launching the bot 67 | retries = 0 68 | 69 | # show the bot is launching 70 | print("Launching Eevee...", end=' ', flush=True) 71 | 72 | while True: 73 | 74 | # call the bot with it's args, and return the exit code 75 | code = subprocess.call(["eevee-bot", *eevee_args]) 76 | 77 | # if clean shutdown 78 | if code == 0: 79 | print("Eevee, goodbye!") 80 | break 81 | 82 | # if restart request 83 | elif code == 26: 84 | print("Rebooting! I'll be back in a bit!\n") 85 | 86 | # tell bot that we're coming back from a restart 87 | if '--fromrestart' not in eevee_args: 88 | eevee_args.append('--fromrestart') 89 | 90 | continue 91 | 92 | # if closed due to error 93 | else: 94 | 95 | # don't retry if specified not to on launch 96 | if launch_args.no_restart: 97 | break 98 | 99 | # increase retry counter 100 | retries += 1 101 | 102 | # calculate wait time with retry count squared, max 10 minutes 103 | wait_time = min([retries**2, 600]) 104 | 105 | # show crash occured in console 106 | print("I crashed!") 107 | if launch_args.debug and retries == 1: 108 | print("Send CTRL+C within 20 seconds to prevent bot restart...") 109 | try: 110 | time.sleep(20) 111 | except KeyboardInterrupt: 112 | print("Exiting.") 113 | sys.exit() 114 | 115 | print("Restarting in...", end='\r', flush=True) 116 | 117 | try: 118 | # each second, update restart counter in console 119 | for i in range(wait_time, 0, -1): 120 | 121 | # if less than a minute, display only seconds 122 | if i < 60: 123 | print(f"Restarting in {i:0d} seconds. ", 124 | end='\r', flush=True) 125 | 126 | # if over a minute, display both mins and secs 127 | else: 128 | m, s = divmod(i, 60) 129 | print(f"Restarting in {m:0d} mins {s:0d} secs.", 130 | end='\r', flush=True) 131 | 132 | # wait a second before updating countdown again 133 | time.sleep(1) 134 | 135 | # show restart attempt at end of countdown 136 | print(" ", 137 | end='\r', flush=True) 138 | print("Restarting now.", 139 | end=' ', flush=True) 140 | 141 | # allow user to interrupt countdown and continue loop immediately 142 | except KeyboardInterrupt: 143 | print(" ", 144 | end='\r', flush=True) 145 | print("Restarting immediately.", 146 | end=' ', flush=True) 147 | 148 | # remove restart flag from any previous attempts 149 | try: 150 | eevee_args.remove('--fromrestart') 151 | except ValueError: 152 | pass 153 | 154 | # when not restarting, show exit code and exit fully 155 | print("Exit code: {exit_code}".format(exit_code=code)) 156 | sys.exit(code) 157 | 158 | if __name__ == '__main__': 159 | main() 160 | -------------------------------------------------------------------------------- /eevee/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .formatters import make_embed, url_color, user_color, cleanup_code, bitround 2 | from .fuzzymatch import get_match 3 | from .enums import ExitCodes 4 | from .datatypes import Map 5 | -------------------------------------------------------------------------------- /eevee/utils/converters.py: -------------------------------------------------------------------------------- 1 | from inspect import signature 2 | from discord.ext import commands 3 | 4 | class BotCommand: 5 | """Convert string to Command object from Bot. 6 | 7 | Returns 8 | -------- 9 | :class:`discord.ext.commands.Command` 10 | """ 11 | @classmethod 12 | async def convert(cls, ctx, arg): 13 | cmd = ctx.bot.get_command(arg) 14 | if cmd is None: 15 | raise commands.BadArgument(f"Command not found.") 16 | return cmd 17 | 18 | class Multi(commands.Converter): 19 | """Convert multiple conversion types. 20 | 21 | Conversion will be attempted in order of first type to last type. 22 | 23 | Parameters 24 | ----------- 25 | *types 26 | Types to be attempted for arg conversion 27 | """ 28 | def __init__(self, *types): 29 | self.types = types 30 | 31 | async def convert(self, ctx, arg): 32 | # credits to MIkusaba 33 | parameters = list(ctx.command.params.values()) 34 | index = min(len(ctx.args) + len(ctx.kwargs), len(parameters) - 1) 35 | param = parameters[index] 36 | print(param) 37 | for type_ in self.types: 38 | try: 39 | return await ctx.command.do_conversion(ctx, type_, arg, param) 40 | except Exception as e: 41 | print(f"Exception {type(e)}: {e}") 42 | type_names = ', '.join([t.__name__ for t in self.types]) 43 | raise commands.BadArgument( 44 | f"{arg} was not able to convert to the following types: " 45 | f"{type_names}") 46 | 47 | class Guild: 48 | """Convert Guild object by ID or name. 49 | 50 | Returns 51 | -------- 52 | :class:`discord.Guild` 53 | """ 54 | @classmethod 55 | async def convert(cls, ctx, arg): 56 | guild = None 57 | if arg.isdigit(): 58 | guild = ctx.bot.get_guild(int(arg)) 59 | if not guild: 60 | guild = ctx.bot.find_guild(arg) 61 | return guild 62 | -------------------------------------------------------------------------------- /eevee/utils/cvtest.py: -------------------------------------------------------------------------------- 1 | from aiocontextvars import Context 2 | 3 | async def context_test_func(number): 4 | print('start context_test_func') 5 | if Context.current().inherited: 6 | print('Inherited!') 7 | ctx_value = _ctx_.get() 8 | print('ctx value is ' + str(ctx_value)) 9 | print('setting ctx value to ' + str(number*1000)) 10 | _ctx_.set(number*1000) 11 | -------------------------------------------------------------------------------- /eevee/utils/datatypes.py: -------------------------------------------------------------------------------- 1 | 2 | class Map(dict): 3 | def __init__(self, *args, **kwargs): 4 | super().__init__(*args, **kwargs) 5 | for arg in args: 6 | if isinstance(arg, dict): 7 | for k, v in arg.items(): 8 | self[k] = v 9 | if kwargs: 10 | for k, v in kwargs.items(): 11 | self[k] = v 12 | 13 | def __getattr__(self, attr): 14 | return self.get(attr) 15 | 16 | def __setattr__(self, key, value): 17 | self.__setitem__(key, value) 18 | 19 | def __setitem__(self, key, value): 20 | super().__setitem__(key, value) 21 | self.__dict__.update({key: value}) 22 | 23 | def __delattr__(self, item): 24 | self.__delitem__(item) 25 | 26 | def __delitem__(self, key): 27 | super().__delitem__(key) 28 | del self.__dict__[key] 29 | -------------------------------------------------------------------------------- /eevee/utils/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class ExitCodes(Enum): 4 | SHUTDOWN = 0 5 | CRITICAL = 1 6 | RESTART = 26 7 | -------------------------------------------------------------------------------- /eevee/utils/formatters.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | 4 | from io import BytesIO 5 | 6 | import aiohttp 7 | 8 | from colorthief import ColorThief 9 | 10 | import discord 11 | 12 | 13 | def cleanup_code(content): 14 | """Automatically removes code blocks from the code.""" 15 | # remove ```py\n``` 16 | if content.startswith('```') and content.endswith('```'): 17 | return '\n'.join(content.split('\n')[1:-1]) 18 | 19 | # remove `foo` 20 | return content.strip('` \n') 21 | 22 | 23 | def colour(*args): 24 | """Returns a discord Colour object. 25 | 26 | Pass one as an argument to define colour: 27 | `int` match colour value. 28 | `str` match common colour names. 29 | `discord.Guild` bot's guild colour. 30 | `None` light grey. 31 | """ 32 | arg = args[0] if args else None 33 | if isinstance(arg, int): 34 | return discord.Colour(arg) 35 | if isinstance(arg, str): 36 | colour = arg 37 | try: 38 | return getattr(discord.Colour, colour)() 39 | except AttributeError: 40 | return discord.Colour.lighter_grey() 41 | if isinstance(arg, discord.Guild): 42 | return arg.me.colour 43 | else: 44 | return discord.Colour.lighter_grey() 45 | 46 | 47 | def make_embed(msg_type='', title=None, icon=None, content=None, 48 | msg_colour=None, guild=None, title_url=None, 49 | thumbnail='', image='', fields=None, footer=None, 50 | footer_icon=None, inline=False): 51 | """Returns a formatted discord embed object. 52 | 53 | Define either a type or a colour. 54 | Types are: 55 | error, warning, info, success, help. 56 | """ 57 | embed_types = { 58 | 'error':{ 59 | 'icon':'https://i.imgur.com/juhq2uJ.png', 60 | 'colour':'red' 61 | }, 62 | 'warning':{ 63 | 'icon':'https://i.imgur.com/4JuaNt9.png', 64 | 'colour':'gold' 65 | }, 66 | 'info':{ 67 | 'icon':'https://i.imgur.com/wzryVaS.png', 68 | 'colour':'blue' 69 | }, 70 | 'success':{ 71 | 'icon':'https://i.imgur.com/ZTKc3mr.png', 72 | 'colour':'green' 73 | }, 74 | 'help':{ 75 | 'icon':'https://i.imgur.com/kTTIZzR.png', 76 | 'colour':'blue' 77 | } 78 | } 79 | if msg_type in embed_types.keys(): 80 | msg_colour = embed_types[msg_type]['colour'] 81 | icon = embed_types[msg_type]['icon'] 82 | if guild and not msg_colour: 83 | msg_colour = colour(guild) 84 | else: 85 | if not isinstance(msg_colour, discord.Colour): 86 | msg_colour = colour(msg_colour) 87 | embed = discord.Embed(description=content, colour=msg_colour) 88 | if not title_url: 89 | title_url = discord.Embed.Empty 90 | if not icon: 91 | icon = discord.Embed.Empty 92 | if title: 93 | embed.set_author(name=title, icon_url=icon, url=title_url) 94 | if thumbnail: 95 | embed.set_thumbnail(url=thumbnail) 96 | if image: 97 | embed.set_image(url=image) 98 | if fields: 99 | for key, value in fields.items(): 100 | ilf = inline 101 | if not isinstance(value, str): 102 | ilf = value[0] 103 | value = value[1] 104 | embed.add_field(name=key, value=value, inline=ilf) 105 | if footer: 106 | footer = {'text':footer} 107 | if footer_icon: 108 | footer['icon_url'] = footer_icon 109 | embed.set_footer(**footer) 110 | return embed 111 | 112 | 113 | async def _read_image_from_url(url): 114 | async with aiohttp.ClientSession() as session: 115 | async with session.get(url) as resp: 116 | return await resp.read() 117 | 118 | 119 | async def _dominant_color_from_url(url): 120 | """Returns an rgb tuple consisting the dominant color given a image url.""" 121 | with BytesIO(await _read_image_from_url(url)) as fp: 122 | loop = asyncio.get_event_loop() 123 | get_colour = functools.partial(ColorThief(fp).get_color, quality=1) 124 | return await loop.run_in_executor(None, get_colour) 125 | 126 | 127 | async def url_color(url): 128 | return discord.Colour.from_rgb(*(await _dominant_color_from_url(url))) 129 | 130 | 131 | async def user_color(user): 132 | return await url_color(user.avatar_url_as(static_format='png')) 133 | 134 | 135 | def bold(msg: str): 136 | """Format to bold markdown text""" 137 | return f'**{msg}**' 138 | 139 | 140 | def italics(msg: str): 141 | """Format to italics markdown text""" 142 | return f'*{msg}*' 143 | 144 | 145 | def bolditalics(msg: str): 146 | """Format to bold italics markdown text""" 147 | return f'***{msg}***' 148 | 149 | 150 | def code(msg: str): 151 | """Format to markdown code block""" 152 | return f'```{msg}```' 153 | 154 | 155 | def pycode(msg: str): 156 | """Format to code block with python code highlighting""" 157 | return f'```py\n{msg}```' 158 | 159 | 160 | def ilcode(msg: str): 161 | """Format to inline markdown code""" 162 | return f'`{msg}`' 163 | 164 | 165 | def convert_to_bool(argument): 166 | lowered = argument.lower() 167 | if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'): 168 | return True 169 | elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'): 170 | return False 171 | else: 172 | return None 173 | 174 | 175 | def bitround(x): 176 | return max(min(1 << int(x).bit_length() - 1, 1024), 16) 177 | -------------------------------------------------------------------------------- /eevee/utils/fuzzymatch.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from fuzzywuzzy import fuzz 4 | from fuzzywuzzy import process 5 | 6 | def get_match(word_list: list, word: str, score_cutoff: int = 60, partial=False): 7 | """Uses fuzzywuzzy to see if word is close to entries in word_list 8 | 9 | Returns a tuple of (MATCH, SCORE) 10 | """ 11 | if partial: 12 | result = process.extractOne( 13 | word, word_list, scorer=fuzz.partial_ratio, score_cutoff=score_cutoff) 14 | else: 15 | result = process.extractOne( 16 | word, word_list, scorer=fuzz.ratio, score_cutoff=score_cutoff) 17 | if not result: 18 | return (None, None) 19 | return result 20 | 21 | def get_matches(word_list: list, word: str, score_cutoff: int = 80, partial=False): 22 | """Uses fuzzywuzzy to see if word is close to entries in word_list 23 | 24 | Returns a list of tuples with (MATCH, SCORE) 25 | """ 26 | if partial: 27 | return process.extractBests( 28 | word, word_list, scorer=fuzz.partial_ratio, score_cutoff=score_cutoff) 29 | return process.extractBests( 30 | word, word_list, scorer=fuzz.ratio, score_cutoff=score_cutoff) 31 | 32 | class FuzzyEnum(Enum): 33 | """Enumeration with fuzzy-matching classmethods.""" 34 | 35 | @classmethod 36 | def name_list(cls): 37 | return list(cls.__members__.keys()) 38 | 39 | @classmethod 40 | def value_list(cls): 41 | return [e.value for e in cls] 42 | 43 | @classmethod 44 | def match_name(cls, arg): 45 | word_list = cls.name_list() 46 | match = get_match(word_list, arg, score_cutoff=80)[0] 47 | return cls[match] 48 | 49 | @classmethod 50 | def match_value(cls, arg): 51 | word_list = cls.value_list() 52 | match = get_match(word_list, arg, score_cutoff=80)[0] 53 | return cls(match) 54 | -------------------------------------------------------------------------------- /eevee/utils/i18n.py: -------------------------------------------------------------------------------- 1 | def temp_str_func(str): 2 | return str 3 | -------------------------------------------------------------------------------- /eevee/utils/pagination.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | import itertools 4 | import re 5 | 6 | import discord 7 | 8 | from eevee.utils import make_embed 9 | 10 | async def _can_run(cmd, ctx): 11 | try: 12 | return await cmd.can_run(ctx) 13 | except: 14 | return False 15 | 16 | class CannotPaginate(Exception): 17 | pass 18 | 19 | class Pagination: 20 | """Implements a paginator that queries the user for the 21 | pagination interface. 22 | 23 | If the user does not reply within 2 minutes then the pagination 24 | interface exits automatically. 25 | """ 26 | def __init__(self, ctx, entries, *, per_page=12, show_entry_count=True, 27 | title='Help', msg_type='help'): 28 | self.bot = ctx.bot 29 | self.entries = entries 30 | self.message = ctx.message 31 | self.channel = ctx.channel 32 | self.author = ctx.author 33 | self.per_page = per_page 34 | pages, left_over = divmod(len(self.entries), self.per_page) 35 | if left_over: 36 | pages += 1 37 | self.maximum_pages = pages 38 | self._mention = re.compile(r'<@\!?([0-9]{1,19})>') 39 | self.embed = make_embed(title=title, msg_type=msg_type) 40 | self.default_icon = self.embed.author.icon_url 41 | self.paginating = len(entries) > per_page 42 | self.show_entry_count = show_entry_count 43 | self.total = len(entries) 44 | self.reaction_emojis = [ 45 | ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', 46 | self.first_page), 47 | ('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page), 48 | ('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page), 49 | ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', 50 | self.last_page), 51 | ('\N{BLACK SQUARE FOR STOP}', self.stop_pages), 52 | ('\N{LEDGER}', self.show_index), 53 | ] 54 | 55 | if ctx.guild is not None: 56 | self.permissions = self.channel.permissions_for(ctx.guild.me) 57 | else: 58 | self.permissions = self.channel.permissions_for(ctx.bot.user) 59 | 60 | if not self.permissions.embed_links: 61 | raise CannotPaginate('Bot does not have embed links permission.') 62 | 63 | if not self.permissions.send_messages: 64 | raise CannotPaginate('Bot cannot send messages.') 65 | 66 | if self.paginating: 67 | if not self.permissions.add_reactions: 68 | raise CannotPaginate( 69 | 'Bot does not have add reactions permission.') 70 | if not self.permissions.read_message_history: 71 | raise CannotPaginate( 72 | 'Bot does not have Read Message History permission.') 73 | 74 | def cleanup_prefix(self, bot, prefix): 75 | m = self._mention.match(prefix) 76 | if m: 77 | user = bot.get_user(int(m.group(1))) 78 | if user: 79 | return f'@{user.name} ' 80 | return prefix 81 | 82 | def _command_signature(self, cmd): 83 | result = [cmd.qualified_name] 84 | if cmd.usage: 85 | result.append(cmd.usage) 86 | return ' '.join(result) 87 | 88 | params = cmd.clean_params 89 | if not params: 90 | return ' '.join(result) 91 | 92 | for name, param in params.items(): 93 | if param.default is not param.empty: 94 | if isinstance(param.default, str): 95 | should_print = param.default 96 | else: 97 | should_print = param.default is not None 98 | if should_print: 99 | result.append(f'[{name}={param.default!r}]') 100 | else: 101 | result.append(f'[{name}]') 102 | elif param.kind == param.VAR_POSITIONAL: 103 | result.append(f'[{name}...]') 104 | else: 105 | result.append(f'<{name}>') 106 | 107 | return ' '.join(result) 108 | 109 | def get_page(self, page): 110 | self.embed.clear_fields() 111 | base = (page - 1) * self.per_page 112 | return self.entries[base:base + self.per_page] 113 | 114 | async def show_page(self, page, *, first=False): 115 | self.current_page = page 116 | entries = self.get_page(page) 117 | 118 | self.embed.clear_fields() 119 | self.embed.title = self.title 120 | self.embed.description = self.description 121 | if self.maximum_pages: 122 | if self.maximum_pages > 1: 123 | self.embed.set_footer(text=( 124 | 'Page {0}/{1} ({2} commands) | ' 125 | 'Use {3}help for more details.' 126 | ).format(page, self.maximum_pages, self.total, self.prefix)) 127 | else: 128 | self.embed.set_footer(text=( 129 | 'Use {}help for more details.').format( 130 | self.prefix)) 131 | else: 132 | self.embed.set_footer(text=( 133 | 'Use {}help for more details.').format(self.prefix)) 134 | 135 | signature = self._command_signature 136 | 137 | requirements = getattr(self, 'requirements', []) 138 | if requirements: 139 | cmd_msg = "" 140 | for req in requirements: 141 | cmd_msg += ( 142 | "{}\n" 143 | ).format(req) 144 | self.embed.add_field( 145 | name="Requirements", value=f"{cmd_msg}", inline=False) 146 | 147 | if entries: 148 | cmd_msg = "" 149 | for entry in entries: 150 | cmd_msg += ( 151 | "{}\n" 152 | ).format(signature(entry)) 153 | self.embed.add_field( 154 | name="Commands", value=f"{cmd_msg}", inline=False) 155 | 156 | if self.maximum_pages: 157 | self.embed.set_author( 158 | icon_url=self.default_icon, 159 | name=f'Help') 160 | 161 | if not self.paginating: 162 | return await self.channel.send(embed=self.embed) 163 | 164 | if not first: 165 | await self.message.edit(embed=self.embed) 166 | return 167 | 168 | self.message = await self.channel.send(embed=self.embed) 169 | for (reaction, __) in self.reaction_emojis: 170 | if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'): 171 | continue 172 | 173 | await self.message.add_reaction(reaction) 174 | 175 | async def checked_show_page(self, page): 176 | if page != 0 and page <= self.maximum_pages: 177 | await self.show_page(page) 178 | 179 | async def first_page(self): 180 | """goes to the first page""" 181 | await self.show_page(1) 182 | 183 | async def last_page(self): 184 | """goes to the last page""" 185 | await self.show_page(self.maximum_pages) 186 | 187 | async def next_page(self): 188 | """goes to the next page""" 189 | await self.checked_show_page(self.current_page + 1) 190 | 191 | async def previous_page(self): 192 | """goes to the previous page""" 193 | await self.checked_show_page(self.current_page - 1) 194 | 195 | async def show_current_page(self): 196 | if self.paginating: 197 | await self.show_page(self.current_page) 198 | 199 | async def numbered_page(self): 200 | """lets you type a page number to go to""" 201 | to_delete = [] 202 | to_delete.append( 203 | await self.channel.send('What page do you want to go to?')) 204 | 205 | def message_check(m): 206 | return m.author == self.author and \ 207 | self.channel == m.channel and \ 208 | m.content.isdigit() 209 | 210 | try: 211 | msg = await self.bot.wait_for( 212 | 'message', check=message_check, timeout=30.0) 213 | except asyncio.TimeoutError: 214 | to_delete.append(await self.channel.send('Took too long.')) 215 | await asyncio.sleep(5) 216 | else: 217 | page = int(msg.content) 218 | to_delete.append(msg) 219 | if page != 0 and page <= self.maximum_pages: 220 | await self.show_page(page) 221 | else: 222 | to_delete.append( 223 | await self.channel.send( 224 | f'Invalid page given. ({page}/{self.maximum_pages})')) 225 | await asyncio.sleep(5) 226 | 227 | try: 228 | await self.channel.delete_messages(to_delete) 229 | except Exception: 230 | pass 231 | 232 | async def show_index(self): 233 | """Shows Page Index""" 234 | 235 | self.embed.title = 'Page Index' 236 | self.embed.description = '' 237 | self.embed.clear_fields() 238 | 239 | pages = {} 240 | for i in range(1, self.maximum_pages + 1): 241 | base = (i - 1) * self.per_page 242 | page_entries = self.entries[base:base + self.per_page] 243 | page_title = f"{page_entries[0][0]} Commands" 244 | page_cmd_count = len(page_entries[0][2]) 245 | pages_entry = pages.get(page_title) 246 | if pages_entry: 247 | pages[page_title]['end_page'] = i 248 | pages[page_title]['cmd_count'] = ( 249 | pages[page_title]['cmd_count']+page_cmd_count) 250 | else: 251 | pages[page_title] = { 252 | 'start_page' : i, 253 | 'cmd_count' : page_cmd_count 254 | } 255 | 256 | for group, data in pages.items(): 257 | cmd_count = data['cmd_count'] 258 | start_page = data['start_page'] 259 | end_page = data.get('end_page') 260 | if end_page: 261 | page_range = f"Pages {start_page} to {end_page}" 262 | else: 263 | page_range = f"Page {start_page}" 264 | self.embed.add_field( 265 | name=group, 266 | value=f"{page_range} ({cmd_count} Commands)") 267 | 268 | self.embed.set_footer(text=f'We were on page {self.current_page}.') 269 | await self.message.edit(embed=self.embed) 270 | 271 | self.reaction_emojis.append( 272 | ('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page)) 273 | await self.message.add_reaction('\N{INPUT SYMBOL FOR NUMBERS}') 274 | 275 | async def go_back_to_current_page(): 276 | await asyncio.sleep(30.0) 277 | await self.show_current_page() 278 | self.bot.loop.create_task(go_back_to_current_page()) 279 | 280 | async def stop_pages(self): 281 | """stops the interactive pagination session""" 282 | await self.message.delete() 283 | self.paginating = False 284 | 285 | def react_check(self, reaction, user): 286 | if user is None or user.id != self.author.id: 287 | return False 288 | 289 | if reaction.message.id != self.message.id: 290 | return False 291 | 292 | for (emoji, func) in self.reaction_emojis: 293 | if reaction.emoji == emoji: 294 | self.match = func 295 | return True 296 | return False 297 | 298 | async def paginate(self): 299 | """Actually paginate the entries and run the interactive loop if 300 | necessary.""" 301 | first_page = self.show_page(1, first=True) 302 | if not self.paginating: 303 | await first_page 304 | else: 305 | self.bot.loop.create_task(first_page) 306 | 307 | while self.paginating: 308 | try: 309 | reaction, user = await self.bot.wait_for( 310 | 'reaction_add', 311 | check=self.react_check, 312 | timeout=120.0) 313 | except asyncio.TimeoutError: 314 | self.paginating = False 315 | try: 316 | await self.message.clear_reactions() 317 | except: 318 | pass 319 | finally: 320 | break 321 | 322 | try: 323 | await self.message.remove_reaction(reaction, user) 324 | except: 325 | pass # can't remove it so don't bother doing so 326 | 327 | try: 328 | self.reaction_emojis.remove( 329 | ('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page)) 330 | await self.message.remove_reaction( 331 | '\N{INPUT SYMBOL FOR NUMBERS}', self.bot.user) 332 | except: 333 | pass # can't remove it so don't bother doing so 334 | 335 | await self.match() 336 | 337 | def get_bot_page(self, page): 338 | cog, description, commands = self.entries[page - 1] 339 | self.title = f'{cog} Commands' 340 | self.description = description 341 | return commands 342 | 343 | @staticmethod 344 | def _command_requirements(cmd): 345 | requirements = [] 346 | for check in cmd.checks: 347 | name = getattr(check, '__qualname__', '') 348 | 349 | if name[:9] == "check_is_": 350 | name = name.replace("check_is_", "").replace("_", " ") 351 | name = f"{name} Only".title() 352 | requirements.append(name) 353 | elif '' in name: 354 | print('yes') 355 | name = name.split('.',1)[0] 356 | name = name.replace("_", " ").title() 357 | requirements.append(name) 358 | else: 359 | requirements.append(name) 360 | 361 | return requirements 362 | 363 | @classmethod 364 | async def from_category(cls, ctx, category): 365 | # get the commands 366 | entries = sorted(ctx.bot.commands, key=lambda c: c.name) 367 | 368 | # get only commands with categories 369 | entries = [ 370 | cmd for cmd in entries if ( 371 | getattr(cmd.callback, 'command_category', False))] 372 | entries = [ 373 | cmd for cmd in entries if ( 374 | cmd.callback.command_category == category)] 375 | 376 | # remove the ones we can't run 377 | entries = [cmd for cmd in entries if ( 378 | await _can_run(cmd, ctx)) and not cmd.hidden] 379 | 380 | self = cls(ctx, entries) 381 | self.title = f'{category} Commands' 382 | 383 | categories = ctx.bot.config.command_categories 384 | cat_cfg = categories.get(category) 385 | 386 | self.description = cat_cfg.get("description") 387 | self.prefix = self.cleanup_prefix(ctx.bot, ctx.prefix) 388 | 389 | return self 390 | 391 | @classmethod 392 | async def from_cog(cls, ctx, cog): 393 | cog_name = cog.__class__.__name__ 394 | 395 | # get the commands 396 | entries = sorted( 397 | ctx.bot.get_cog_commands(cog_name), key=lambda c: c.name) 398 | 399 | # remove the ones we can't run 400 | entries = [cmd for cmd in entries if ( 401 | await _can_run(cmd, ctx)) and not cmd.hidden] 402 | 403 | self = cls(ctx, entries) 404 | self.title = f'{cog_name} Commands' 405 | self.description = inspect.getdoc(cog) 406 | self.prefix = self.cleanup_prefix(ctx.bot, ctx.prefix) 407 | 408 | return self 409 | 410 | @classmethod 411 | async def from_command(cls, ctx, command, **kwargs): 412 | try: 413 | entries = sorted(command.commands, key=lambda c: c.name) 414 | except AttributeError: 415 | entries = [] 416 | else: 417 | entries = [cmd for cmd in entries if ( 418 | await _can_run(cmd, ctx)) and not cmd.hidden] 419 | 420 | self = cls(ctx, entries, **kwargs) 421 | self.title = command.signature 422 | 423 | if command.description: 424 | self.description = f'{command.description}\n\n{command.help}' 425 | else: 426 | self.description = command.help or 'No help given.' 427 | 428 | if command.checks: 429 | self.requirements = self._command_requirements(command) 430 | 431 | self.prefix = self.cleanup_prefix(ctx.bot, ctx.prefix) 432 | return self 433 | 434 | @classmethod 435 | async def from_bot(cls, ctx): 436 | 437 | categories = ctx.bot.config.command_categories 438 | 439 | def sortkey(cmd): 440 | category = getattr(cmd.callback, 'command_category', None) 441 | cat_cfg = categories.get(category) 442 | category = cat_cfg["index"] if cat_cfg else category 443 | return category or cmd.cog_name or '\u200bMisc' 444 | 445 | def groupkey(cmd): 446 | category = getattr(cmd.callback, 'command_category', None) 447 | return category or cmd.cog_name or '\u200bMisc' 448 | 449 | entries = sorted(ctx.bot.commands, key=sortkey) 450 | nested_pages = [] 451 | per_page = 15 452 | 453 | for cog, commands in itertools.groupby(entries, key=groupkey): 454 | plausible = [cmd for cmd in commands if ( 455 | await _can_run(cmd, ctx)) and not cmd.hidden] 456 | if len(plausible) == 0: 457 | continue 458 | plausible = sorted(plausible, key=lambda x: x.name) 459 | description = ctx.bot.get_cog(cog) 460 | if description is None: 461 | cat_cfg = categories.get(cog) 462 | if cat_cfg: 463 | description = cat_cfg.get("description") 464 | else: 465 | description = discord.Embed.Empty 466 | else: 467 | description = ( 468 | inspect.getdoc(description) or discord.Embed.Empty) 469 | 470 | nested_pages.extend( 471 | (cog, description, plausible[i:i + per_page]) for i in range( 472 | 0, len(plausible), per_page)) 473 | 474 | # this forces the pagination session 475 | self = cls(ctx, nested_pages, per_page=1) 476 | self.prefix = self.cleanup_prefix(ctx.bot, ctx.prefix) 477 | 478 | # swap the get_page implementation 479 | self.get_page = self.get_bot_page 480 | self._is_bot = True 481 | 482 | # replace the actual total 483 | self.total = sum(len(o) for __, __, o in nested_pages) 484 | return self 485 | 486 | cls.reaction_emojis.append( 487 | ('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page)) 488 | await self.message.add_reaction('\N{INPUT SYMBOL FOR NUMBERS}') 489 | -------------------------------------------------------------------------------- /eevee/utils/snowflake.py: -------------------------------------------------------------------------------- 1 | """Create 64bit unique IDs with timestamps embedded.""" 2 | 3 | import time 4 | 5 | # 07 Aug 2017 16:26:00 GMT 6 | EEVEEPOCH = 1502123160 7 | 8 | 9 | def to_timestamp(_id): 10 | _id = _id >> 22 # strip the lower 22 bits 11 | _id += EEVEEPOCH # adjust for eevee epoch 12 | _id = _id // 1000 # convert from milliseconds to seconds 13 | return _id 14 | 15 | 16 | def create(): 17 | """Create and return snowflake for current time.""" 18 | sleep = lambda x: time.sleep(x/1000.0) 19 | worker_id = 1 20 | data_center_id = 1 21 | worker_id_bits = 5 22 | data_center_id_bits = 5 23 | max_worker_id = -1 ^ (-1 << worker_id_bits) 24 | max_data_center_id = -1 ^ (-1 << data_center_id_bits) 25 | sequence_bits = 12 26 | worker_id_shift = sequence_bits 27 | data_center_id_shift = sequence_bits + worker_id_bits 28 | timestamp_left_shift = sequence_bits + worker_id_bits + data_center_id_bits 29 | sequence_mask = -1 ^ (-1 << sequence_bits) 30 | 31 | assert worker_id >= 0 and worker_id <= max_worker_id 32 | assert data_center_id >= 0 and data_center_id <= max_data_center_id 33 | 34 | last_timestamp = -1 35 | sequence = 0 36 | 37 | while True: 38 | timestamp = int(time.time()*1000) 39 | if last_timestamp > timestamp: 40 | # clock is moving backwards. waiting until last_timestamp 41 | sleep(last_timestamp-timestamp) 42 | continue 43 | 44 | if last_timestamp == timestamp: 45 | sequence = (sequence + 1) & sequence_mask 46 | if sequence == 0: 47 | # sequence overrun 48 | sequence = -1 & sequence_mask 49 | sleep(1) 50 | continue 51 | else: 52 | sequence = 0 53 | 54 | last_timestamp = timestamp 55 | 56 | yield ( 57 | ((timestamp-EEVEEPOCH) << timestamp_left_shift) | 58 | (data_center_id << data_center_id_shift) | 59 | (worker_id << worker_id_shift) | 60 | sequence) 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name='eevee', 5 | version='1.0.0a0', 6 | description='A Discord Bot Framework.', 7 | url='https://github.com/scragly/Eevee', 8 | author='Scragly', 9 | author_email='', 10 | license='GNU General Public License v3.0', 11 | 12 | classifiers=[ 13 | 'Development Status :: 2 - Pre-Alpha', 14 | 'Intended Audience :: Developers', 15 | 'Topic :: Communications :: Chat', 16 | 'Topic :: Utilities', 17 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 18 | 'Programming Language :: Python :: 3.6', 19 | ], 20 | 21 | keywords='discord chat bot development framework', 22 | 23 | install_requires=[ 24 | 'discord.py', 25 | 'python-dateutil>=2.6', 26 | 'asyncpg>=0.13', 27 | 'python-Levenshtein>=0.12', 28 | 'fuzzywuzzy', 29 | 'psutil', 30 | 'aiocontextvars', 31 | 'colorthief', 32 | 'more_itertools', 33 | 'numpy', 34 | 'pendulum', 35 | 'pytz' 36 | ], 37 | 38 | dependency_links=[ 39 | 'git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py-1' 40 | ], 41 | 42 | package_data={ 43 | 'eevee': ['data/*.json'], 44 | }, 45 | 46 | entry_points={ 47 | 'console_scripts': [ 48 | 'eevee=eevee.launcher:main', 49 | 'eevee-bot=eevee.__main__:main' 50 | ], 51 | }, 52 | ) 53 | --------------------------------------------------------------------------------