diff options
| author | 2020-09-22 11:51:02 +0200 | |
|---|---|---|
| committer | 2020-09-22 11:51:02 +0200 | |
| commit | 6d38e0a61924cae69c544c07955567d9a2c1fc18 (patch) | |
| tree | 218dcc50c86dcfa9ece7e39116ba1d77ed33ae82 | |
| parent | corrected value error msg (diff) | |
| parent | Merge pull request #448 from python-discord/clean_uwu (diff) | |
Merge branch 'master' into master
Diffstat (limited to '')
| -rw-r--r-- | Pipfile | 1 | ||||
| -rw-r--r-- | Pipfile.lock | 146 | ||||
| -rw-r--r-- | bot/constants.py | 23 | ||||
| -rw-r--r-- | bot/exts/easter/conversationstarters.py | 28 | ||||
| -rw-r--r-- | bot/exts/evergreen/conversationstarters.py | 71 | ||||
| -rw-r--r-- | bot/exts/evergreen/fun.py | 139 | ||||
| -rw-r--r-- | bot/exts/evergreen/issues.py | 113 | ||||
| -rw-r--r-- | bot/exts/evergreen/reddit.py | 6 | ||||
| -rw-r--r-- | bot/exts/evergreen/wolfram.py | 278 | ||||
| -rw-r--r-- | bot/resources/easter/starter.json | 24 | ||||
| -rw-r--r-- | bot/resources/evergreen/caesar_info.json | 4 | ||||
| -rw-r--r-- | bot/resources/evergreen/py_topics.yaml | 89 | ||||
| -rw-r--r-- | bot/resources/evergreen/starter.yaml | 22 | ||||
| -rw-r--r-- | bot/resources/evergreen/trivia_quiz.json | 30 | ||||
| -rw-r--r-- | bot/utils/randomization.py | 23 | 
15 files changed, 809 insertions, 188 deletions
| @@ -12,6 +12,7 @@ fuzzywuzzy = "~=0.17"  pillow = "~=6.2"  pytz = "~=2019.2"  sentry-sdk = "~=0.14.2" +PyYAML = "~=5.3.1"  [dev-packages]  flake8 = "~=3.8" diff --git a/Pipfile.lock b/Pipfile.lock index c9ee97a1..74931967 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@  {      "_meta": {          "hash": { -            "sha256": "608dbc82dfdab7596c3e1eb18e05f90d596230b29af25c7e7a2c351196a6d045" +            "sha256": "dfd24795dcdab1a05fd774b5034b195b69d6d6ed52fd6810db9c89247f8e0a43"          },          "pipfile-spec": 6,          "requires": { @@ -44,11 +44,11 @@          },          "arrow": {              "hashes": [ -                "sha256:3f1a92b25bbee5f80cc8f6bdecfeade9028219229137c559c37335b4f574a292", -                "sha256:61a1af3a31f731e7993509124839ac28b91b6743bd6692a949600737900cf43b" +                "sha256:271b8e05174d48e50324ed0dc5d74796c839c7e579a4f21cf1a7394665f9e94f", +                "sha256:edc31dc051db12c95da9bac0271cd1027b8e36912daf6d4580af53b23e62721a"              ],              "index": "pypi", -            "version": "==0.15.7" +            "version": "==0.15.8"          },          "async-timeout": {              "hashes": [ @@ -84,36 +84,36 @@          },          "cffi": {              "hashes": [ -                "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", -                "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", -                "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", -                "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", -                "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", -                "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", -                "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", -                "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", -                "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", -                "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", -                "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", -                "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", -                "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", -                "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", -                "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", -                "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", -                "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", -                "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", -                "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", -                "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", -                "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", -                "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", -                "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", -                "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", -                "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", -                "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", -                "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", -                "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" -            ], -            "version": "==1.14.0" +                "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e", +                "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c", +                "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e", +                "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1", +                "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4", +                "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2", +                "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c", +                "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0", +                "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798", +                "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1", +                "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4", +                "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731", +                "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4", +                "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c", +                "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487", +                "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e", +                "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f", +                "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123", +                "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c", +                "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b", +                "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650", +                "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad", +                "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75", +                "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82", +                "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7", +                "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15", +                "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa", +                "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281" +            ], +            "version": "==1.14.2"          },          "chardet": {              "hashes": [ @@ -259,6 +259,23 @@              "index": "pypi",              "version": "==2019.3"          }, +        "pyyaml": { +            "hashes": [ +                "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", +                "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", +                "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", +                "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", +                "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", +                "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", +                "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", +                "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", +                "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", +                "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", +                "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" +            ], +            "index": "pypi", +            "version": "==5.3.1" +        },          "sentry-sdk": {              "hashes": [                  "sha256:0e5e947d0f7a969314aa23669a94a9712be5a688ff069ff7b9fc36c66adc160c", @@ -285,11 +302,11 @@          },          "urllib3": {              "hashes": [ -                "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", -                "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" +                "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", +                "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", -            "version": "==1.25.9" +            "version": "==1.25.10"          },          "websockets": {              "hashes": [ @@ -321,26 +338,26 @@          },          "yarl": {              "hashes": [ -                "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", -                "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", -                "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", -                "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", -                "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", -                "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", -                "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", -                "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", -                "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", -                "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", -                "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", -                "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", -                "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", -                "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", -                "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", -                "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", -                "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" +                "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", +                "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", +                "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", +                "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", +                "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", +                "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", +                "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", +                "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", +                "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", +                "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", +                "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", +                "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", +                "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", +                "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", +                "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", +                "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", +                "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6"              ],              "markers": "python_version >= '3.5'", -            "version": "==1.4.2" +            "version": "==1.5.1"          }      },      "develop": { @@ -361,11 +378,11 @@          },          "cfgv": {              "hashes": [ -                "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53", -                "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513" +                "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", +                "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"              ],              "markers": "python_full_version >= '3.6.1'", -            "version": "==3.1.0" +            "version": "==3.2.0"          },          "distlib": {              "hashes": [ @@ -453,11 +470,11 @@          },          "identify": {              "hashes": [ -                "sha256:882c4b08b4569517b5f2257ecca180e01f38400a17f429f5d0edff55530c41c7", -                "sha256:f89add935982d5bc62913ceee16c9297d8ff14b226e9d3072383a4e38136b656" +                "sha256:69c4769f085badafd0e04b1763e847258cbbf6d898e8678ebffc91abdb86f6c6", +                "sha256:d6ae6daee50ba1b493e9ca4d36a5edd55905d2cf43548fdc20b2a14edef102e7"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==1.4.23" +            "version": "==1.4.28"          },          "mccabe": {              "hashes": [ @@ -526,6 +543,7 @@                  "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",                  "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"              ], +            "index": "pypi",              "version": "==5.3.1"          },          "six": { @@ -552,11 +570,11 @@          },          "virtualenv": {              "hashes": [ -                "sha256:c11a475400e98450403c0364eb3a2d25d42f71cf1493da64390487b666de4324", -                "sha256:e10cc66f40cbda459720dfe1d334c4dc15add0d80f09108224f171006a97a172" +                "sha256:7b54fd606a1b85f83de49ad8d80dbec08e983a2d2f96685045b262ebc7481ee5", +                "sha256:8cd7b2a4850b003a11be2fc213e206419efab41115cc14bca20e69654f2ac08e"              ],              "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", -            "version": "==20.0.26" +            "version": "==20.0.30"          }      }  } diff --git a/bot/constants.py b/bot/constants.py index 133db56c..7c8f72cb 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -17,6 +17,7 @@ __all__ = (      "Month",      "Roles",      "Tokens", +    "Wolfram",      "MODERATION_ROLES",      "STAFF_ROLES",      "WHITELISTED_CHANNELS", @@ -51,6 +52,7 @@ class Channels(NamedTuple):      devalerts = 460181980097675264      devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554))      dev_contrib = 635950537262759947 +    dev_branding = 753252897059373066      help_0 = 303906576991780866      help_1 = 303906556754395136      help_2 = 303906514266226689 @@ -92,10 +94,11 @@ class Colours:      dark_green = 0x1f8b4c      orange = 0xe67e22      pink = 0xcf84e0 +    purple = 0xb734eb      soft_green = 0x68c290 +    soft_orange = 0xf9cb54      soft_red = 0xcd6d6d      yellow = 0xf9f586 -    purple = 0xb734eb  class Emojis: @@ -106,12 +109,12 @@ class Emojis:      trashcan = "<:trashcan:637136429717389331>"      ok_hand = ":ok_hand:" -    terning1 = "<:terning1:431249668983488527>" -    terning2 = "<:terning2:462339216987127808>" -    terning3 = "<:terning3:431249694467948544>" -    terning4 = "<:terning4:579980271475228682>" -    terning5 = "<:terning5:431249716328792064>" -    terning6 = "<:terning6:431249726705369098>" +    dice_1 = "<:dice_1:755891608859443290>" +    dice_2 = "<:dice_2:755891608741740635>" +    dice_3 = "<:dice_3:755891608251138158>" +    dice_4 = "<:dice_4:755891607882039327>" +    dice_5 = "<:dice_5:755891608091885627>" +    dice_6 = "<:dice_6:755891607680843838>"      issue = "<:IssueOpen:629695470327037963>"      issue_closed = "<:IssueClosed:629695470570307614>" @@ -187,6 +190,12 @@ class Tokens(NamedTuple):      github = environ.get("GITHUB_TOKEN") +class Wolfram(NamedTuple): +    user_limit_day = int(environ.get("WOLFRAM_USER_LIMIT_DAY", 10)) +    guild_limit_day = int(environ.get("WOLFRAM_GUILD_LIMIT_DAY", 67)) +    key = environ.get("WOLFRAM_API_KEY") + +  # Default role combinations  MODERATION_ROLES = Roles.moderator, Roles.admin, Roles.owner  STAFF_ROLES = Roles.helpers, Roles.moderator, Roles.admin, Roles.owner diff --git a/bot/exts/easter/conversationstarters.py b/bot/exts/easter/conversationstarters.py deleted file mode 100644 index a5f40445..00000000 --- a/bot/exts/easter/conversationstarters.py +++ /dev/null @@ -1,28 +0,0 @@ -import json -import logging -import random -from pathlib import Path - -from discord.ext import commands - -log = logging.getLogger(__name__) - -with open(Path("bot/resources/easter/starter.json"), "r", encoding="utf8") as f: -    starters = json.load(f) - - -class ConvoStarters(commands.Cog): -    """Easter conversation topics.""" - -    def __init__(self, bot: commands.Bot): -        self.bot = bot - -    @commands.command() -    async def topic(self, ctx: commands.Context) -> None: -        """Responds with a random topic to start a conversation.""" -        await ctx.send(random.choice(starters['starters'])) - - -def setup(bot: commands.Bot) -> None: -    """Conversation starters Cog load.""" -    bot.add_cog(ConvoStarters(bot)) diff --git a/bot/exts/evergreen/conversationstarters.py b/bot/exts/evergreen/conversationstarters.py new file mode 100644 index 00000000..576b8d76 --- /dev/null +++ b/bot/exts/evergreen/conversationstarters.py @@ -0,0 +1,71 @@ +from pathlib import Path + +import yaml +from discord import Color, Embed +from discord.ext import commands + +from bot.constants import WHITELISTED_CHANNELS +from bot.utils.decorators import override_in_channel +from bot.utils.randomization import RandomCycle + +SUGGESTION_FORM = 'https://forms.gle/zw6kkJqv8U43Nfjg9' + +with Path("bot/resources/evergreen/starter.yaml").open("r", encoding="utf8") as f: +    STARTERS = yaml.load(f, Loader=yaml.FullLoader) + +with Path("bot/resources/evergreen/py_topics.yaml").open("r", encoding="utf8") as f: +    # First ID is #python-general and the rest are top to bottom categories of Topical Chat/Help. +    PY_TOPICS = yaml.load(f, Loader=yaml.FullLoader) + +    # Removing `None` from lists of topics, if not a list, it is changed to an empty one. +    PY_TOPICS = {k: [i for i in v if i] if isinstance(v, list) else [] for k, v in PY_TOPICS.items()} + +    # All the allowed channels that the ".topic" command is allowed to be executed in. +    ALL_ALLOWED_CHANNELS = list(PY_TOPICS.keys()) + list(WHITELISTED_CHANNELS) + +# Putting all topics into one dictionary and shuffling lists to reduce same-topic repetitions. +ALL_TOPICS = {'default': STARTERS, **PY_TOPICS} +TOPICS = { +    channel: RandomCycle(topics or ['No topics found for this channel.']) +    for channel, topics in ALL_TOPICS.items() +} + + +class ConvoStarters(commands.Cog): +    """Evergreen conversation topics.""" + +    def __init__(self, bot: commands.Bot): +        self.bot = bot + +    @commands.command() +    @override_in_channel(ALL_ALLOWED_CHANNELS) +    async def topic(self, ctx: commands.Context) -> None: +        """ +        Responds with a random topic to start a conversation. + +        If in a Python channel, a python-related topic will be given. + +        Otherwise, a random conversation topic will be received by the user. +        """ +        # No matter what, the form will be shown. +        embed = Embed(description=f'Suggest more topics [here]({SUGGESTION_FORM})!', color=Color.blurple()) + +        try: +            # Fetching topics. +            channel_topics = TOPICS[ctx.channel.id] + +        # If the channel isn't Python-related. +        except KeyError: +            embed.title = f'**{next(TOPICS["default"])}**' + +        # If the channel ID doesn't have any topics. +        else: +            embed.title = f'**{next(channel_topics)}**' + +        finally: +            await ctx.send(embed=embed) + + +def setup(bot: commands.Bot) -> None: +    """Conversation starters Cog load.""" +    bot.add_cog(ConvoStarters(bot)) diff --git a/bot/exts/evergreen/fun.py b/bot/exts/evergreen/fun.py index 67a4bae5..de6a92c6 100644 --- a/bot/exts/evergreen/fun.py +++ b/bot/exts/evergreen/fun.py @@ -1,14 +1,16 @@  import functools +import json  import logging  import random -from typing import Callable, Tuple, Union +from pathlib import Path +from typing import Callable, Iterable, Tuple, Union  from discord import Embed, Message  from discord.ext import commands -from discord.ext.commands import Bot, Cog, Context, MessageConverter +from discord.ext.commands import Bot, Cog, Context, MessageConverter, clean_content  from bot import utils -from bot.constants import Emojis +from bot.constants import Colours, Emojis  log = logging.getLogger(__name__) @@ -26,12 +28,35 @@ UWU_WORDS = {  } +def caesar_cipher(text: str, offset: int) -> Iterable[str]: +    """ +    Implements a lazy Caesar Cipher algorithm. + +    Encrypts a `text` given a specific integer `offset`. The sign +    of the `offset` dictates the direction in which it shifts to, +    with a negative value shifting to the left, and a positive +    value shifting to the right. +    """ +    for char in text: +        if not char.isascii() or not char.isalpha() or char.isspace(): +            yield char +            continue + +        case_start = 65 if char.isupper() else 97 +        true_offset = (ord(char) - case_start + offset) % 26 + +        yield chr(case_start + true_offset) + +  class Fun(Cog):      """A collection of general commands for fun."""      def __init__(self, bot: Bot) -> None:          self.bot = bot +        with Path("bot/resources/evergreen/caesar_info.json").open("r", encoding="UTF-8") as f: +            self._caesar_cipher_embed = json.load(f) +      @commands.command()      async def roll(self, ctx: Context, num_rolls: int = 1) -> None:          """Outputs a number of random dice emotes (up to 6).""" @@ -41,17 +66,13 @@ class Fun(Cog):          elif num_rolls < 1:              output = ":no_entry: You must roll at least once."          for _ in range(num_rolls): -            terning = f"terning{random.randint(1, 6)}" -            output += getattr(Emojis, terning, '') +            dice = f"dice_{random.randint(1, 6)}" +            output += getattr(Emojis, dice, '')          await ctx.send(output)      @commands.command(name="uwu", aliases=("uwuwize", "uwuify",)) -    async def uwu_command(self, ctx: Context, *, text: str) -> None: -        """ -        Converts a given `text` into it's uwu equivalent. - -        Also accepts a valid discord Message ID or link. -        """ +    async def uwu_command(self, ctx: Context, *, text: clean_content(fix_channel_mentions=True)) -> None: +        """Converts a given `text` into it's uwu equivalent."""          conversion_func = functools.partial(              utils.replace_many, replacements=UWU_WORDS, ignore_case=True, match_case=True          ) @@ -66,12 +87,8 @@ class Fun(Cog):          await ctx.send(content=converted_text, embed=embed)      @commands.command(name="randomcase", aliases=("rcase", "randomcaps", "rcaps",)) -    async def randomcase_command(self, ctx: Context, *, text: str) -> None: -        """ -        Randomly converts the casing of a given `text`. - -        Also accepts a valid discord Message ID or link. -        """ +    async def randomcase_command(self, ctx: Context, *, text: clean_content(fix_channel_mentions=True)) -> None: +        """Randomly converts the casing of a given `text`."""          def conversion_func(text: str) -> str:              """Randomly converts the casing of a given string."""              return "".join( @@ -87,22 +104,100 @@ class Fun(Cog):              converted_text = f">>> {converted_text.lstrip('> ')}"          await ctx.send(content=converted_text, embed=embed) +    @commands.group(name="caesarcipher", aliases=("caesar", "cc",)) +    async def caesarcipher_group(self, ctx: Context) -> None: +        """ +        Translates a message using the Caesar Cipher. + +        See `decrypt`, `encrypt`, and `info` subcommands. +        """ +        if ctx.invoked_subcommand is None: +            await ctx.invoke(self.bot.get_command("help"), "caesarcipher") + +    @caesarcipher_group.command(name="info") +    async def caesarcipher_info(self, ctx: Context) -> None: +        """Information about the Caesar Cipher.""" +        embed = Embed.from_dict(self._caesar_cipher_embed) +        embed.colour = Colours.dark_green + +        await ctx.send(embed=embed) + +    @staticmethod +    async def _caesar_cipher(ctx: Context, offset: int, msg: str, left_shift: bool = False) -> None: +        """ +        Given a positive integer `offset`, translates and sends the given `msg`. + +        Performs a right shift by default unless `left_shift` is specified as `True`. + +        Also accepts a valid Discord Message ID or link. +        """ +        if offset < 0: +            await ctx.send(":no_entry: Cannot use a negative offset.") +            return + +        if left_shift: +            offset = -offset + +        def conversion_func(text: str) -> str: +            """Encrypts the given string using the Caesar Cipher.""" +            return "".join(caesar_cipher(text, offset)) + +        text, embed = await Fun._get_text_and_embed(ctx, msg) + +        if embed is not None: +            embed = Fun._convert_embed(conversion_func, embed) + +        converted_text = conversion_func(text) + +        if converted_text: +            converted_text = f">>> {converted_text.lstrip('> ')}" + +        await ctx.send(content=converted_text, embed=embed) + +    @caesarcipher_group.command(name="encrypt", aliases=("rightshift", "rshift", "enc",)) +    async def caesarcipher_encrypt(self, ctx: Context, offset: int, *, msg: str) -> None: +        """ +        Given a positive integer `offset`, encrypt the given `msg`. + +        Performs a right shift of the letters in the message. + +        Also accepts a valid Discord Message ID or link. +        """ +        await self._caesar_cipher(ctx, offset, msg, left_shift=False) + +    @caesarcipher_group.command(name="decrypt", aliases=("leftshift", "lshift", "dec",)) +    async def caesarcipher_decrypt(self, ctx: Context, offset: int, *, msg: str) -> None: +        """ +        Given a positive integer `offset`, decrypt the given `msg`. + +        Performs a left shift of the letters in the message. + +        Also accepts a valid Discord Message ID or link. +        """ +        await self._caesar_cipher(ctx, offset, msg, left_shift=True) +      @staticmethod      async def _get_text_and_embed(ctx: Context, text: str) -> Tuple[str, Union[Embed, None]]:          """          Attempts to extract the text and embed from a possible link to a discord Message. +        Does not retrieve the text and embed from the Message if it is in a channel the user does +        not have read permissions in. +          Returns a tuple of:              str: If `text` is a valid discord Message, the contents of the message, else `text`.              Union[Embed, None]: The embed if found in the valid Message, else None          """          embed = None -        message = await Fun._get_discord_message(ctx, text) -        if isinstance(message, Message): -            text = message.content + +        msg = await Fun._get_discord_message(ctx, text) +        # Ensure the user has read permissions for the channel the message is in +        if isinstance(msg, Message) and ctx.author.permissions_in(msg.channel).read_messages: +            text = msg.clean_content              # Take first embed because we can't send multiple embeds -            if message.embeds: -                embed = message.embeds[0] +            if msg.embeds: +                embed = msg.embeds[0] +          return (text, embed)      @staticmethod diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 0f83731b..5a5c82e7 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -1,9 +1,10 @@  import logging +import random  import discord  from discord.ext import commands -from bot.constants import Channels, Colours, Emojis, WHITELISTED_CHANNELS +from bot.constants import Channels, Colours, ERROR_REPLIES, Emojis, Tokens, WHITELISTED_CHANNELS  from bot.utils.decorators import override_in_channel  log = logging.getLogger(__name__) @@ -13,6 +14,12 @@ BAD_RESPONSE = {      403: "Rate limit has been hit! Please try again later!"  } +MAX_REQUESTS = 10 + +REQUEST_HEADERS = dict() +if GITHUB_TOKEN := Tokens.github: +    REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" +  class Issues(commands.Cog):      """Cog that allows users to retrieve issues from GitHub.""" @@ -21,53 +28,79 @@ class Issues(commands.Cog):          self.bot = bot      @commands.command(aliases=("pr",)) -    @override_in_channel(WHITELISTED_CHANNELS + (Channels.dev_contrib,)) +    @override_in_channel(WHITELISTED_CHANNELS + (Channels.dev_contrib, Channels.dev_branding))      async def issue( -        self, ctx: commands.Context, number: int, repository: str = "seasonalbot", user: str = "python-discord" +        self, +        ctx: commands.Context, +        numbers: commands.Greedy[int], +        repository: str = "seasonalbot", +        user: str = "python-discord"      ) -> None: -        """Command to retrieve issues from a GitHub repository.""" -        url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}" -        merge_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge" - -        log.trace(f"Querying GH issues API: {url}") -        async with self.bot.http_session.get(url) as r: -            json_data = await r.json() - -        if r.status in BAD_RESPONSE: -            log.warning(f"Received response {r.status} from: {url}") -            return await ctx.send(f"[{str(r.status)}] {BAD_RESPONSE.get(r.status)}") - -        # The initial API request is made to the issues API endpoint, which will return information -        # if the issue or PR is present. However, the scope of information returned for PRs differs -        # from issues: if the 'issues' key is present in the response then we can pull the data we -        # need from the initial API call. -        if "issues" in json_data.get("html_url"): -            if json_data.get("state") == "open": -                icon_url = Emojis.issue -            else: -                icon_url = Emojis.issue_closed - -        # If the 'issues' key is not contained in the API response and there is no error code, then -        # we know that a PR has been requested and a call to the pulls API endpoint is necessary -        # to get the desired information for the PR. -        else: -            log.trace(f"PR provided, querying GH pulls API for additional information: {merge_url}") -            async with self.bot.http_session.get(merge_url) as m: +        """Command to retrieve issue(s) from a GitHub repository.""" +        links = [] +        numbers = set(numbers) + +        if not numbers: +            await ctx.invoke(self.bot.get_command('help'), 'issue') +            return + +        if len(numbers) > MAX_REQUESTS: +            embed = discord.Embed( +                title=random.choice(ERROR_REPLIES), +                color=Colours.soft_red, +                description=f"Too many issues/PRs! (maximum of {MAX_REQUESTS})" +            ) +            await ctx.send(embed=embed) +            return + +        for number in set(numbers): +            # Convert from list to set to remove duplicates, if any. +            url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}" +            merge_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge" + +            log.trace(f"Querying GH issues API: {url}") +            async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as r: +                json_data = await r.json() + +            if r.status in BAD_RESPONSE: +                log.warning(f"Received response {r.status} from: {url}") +                return await ctx.send(f"[{str(r.status)}] #{number} {BAD_RESPONSE.get(r.status)}") + +            # The initial API request is made to the issues API endpoint, which will return information +            # if the issue or PR is present. However, the scope of information returned for PRs differs +            # from issues: if the 'issues' key is present in the response then we can pull the data we +            # need from the initial API call. +            if "issues" in json_data.get("html_url"):                  if json_data.get("state") == "open": -                    icon_url = Emojis.pull_request -                # When the status is 204 this means that the state of the PR is merged -                elif m.status == 204: -                    icon_url = Emojis.merge +                    icon_url = Emojis.issue                  else: -                    icon_url = Emojis.pull_request_closed +                    icon_url = Emojis.issue_closed + +            # If the 'issues' key is not contained in the API response and there is no error code, then +            # we know that a PR has been requested and a call to the pulls API endpoint is necessary +            # to get the desired information for the PR. +            else: +                log.trace(f"PR provided, querying GH pulls API for additional information: {merge_url}") +                async with self.bot.http_session.get(merge_url) as m: +                    if json_data.get("state") == "open": +                        icon_url = Emojis.pull_request +                    # When the status is 204 this means that the state of the PR is merged +                    elif m.status == 204: +                        icon_url = Emojis.merge +                    else: +                        icon_url = Emojis.pull_request_closed + +            issue_url = json_data.get("html_url") +            links.append([icon_url, f"[{repository}] #{number} {json_data.get('title')}", issue_url]) -        issue_url = json_data.get("html_url") -        description_text = f"[{repository}] #{number} {json_data.get('title')}" +        # Issue/PR format: emoji to show if open/closed/merged, number and the title as a singular link. +        description_list = ["{0} [{1}]({2})".format(*link) for link in links]          resp = discord.Embed(              colour=Colours.bright_green, -            description=f"{icon_url} [{description_text}]({issue_url})" +            description='\n'.join(description_list)          ) -        resp.set_author(name="GitHub", url=issue_url) + +        resp.set_author(name="GitHub", url=f"https://github.com/{user}/{repository}")          await ctx.send(embed=resp) diff --git a/bot/exts/evergreen/reddit.py b/bot/exts/evergreen/reddit.py index fe204419..49127bea 100644 --- a/bot/exts/evergreen/reddit.py +++ b/bot/exts/evergreen/reddit.py @@ -68,9 +68,9 @@ class Reddit(commands.Cog):          # -----------------------------------------------------------          # This code below is bound of change when the emojis are added. -        upvote_emoji = self.bot.get_emoji(638729835245731840) -        comment_emoji = self.bot.get_emoji(638729835073765387) -        user_emoji = self.bot.get_emoji(638729835442602003) +        upvote_emoji = self.bot.get_emoji(755845219890757644) +        comment_emoji = self.bot.get_emoji(755845255001014384) +        user_emoji = self.bot.get_emoji(755845303822974997)          text_emoji = self.bot.get_emoji(676030265910493204)          video_emoji = self.bot.get_emoji(676030265839190047)          image_emoji = self.bot.get_emoji(676030265734201344) diff --git a/bot/exts/evergreen/wolfram.py b/bot/exts/evergreen/wolfram.py new file mode 100644 index 00000000..898e8d2a --- /dev/null +++ b/bot/exts/evergreen/wolfram.py @@ -0,0 +1,278 @@ +import logging +from io import BytesIO +from typing import Callable, List, Optional, Tuple +from urllib import parse + +import arrow +import discord +from discord import Embed +from discord.ext import commands +from discord.ext.commands import BucketType, Cog, Context, check, group + +from bot.constants import Colours, STAFF_ROLES, Wolfram +from bot.utils.pagination import ImagePaginator + +log = logging.getLogger(__name__) + +APPID = Wolfram.key +DEFAULT_OUTPUT_FORMAT = "JSON" +QUERY = "http://api.wolframalpha.com/v2/{request}?{data}" +WOLF_IMAGE = "https://www.symbols.com/gi.php?type=1&id=2886&i=1" + +MAX_PODS = 20 + +# Allows for 10 wolfram calls pr user pr day +usercd = commands.CooldownMapping.from_cooldown(Wolfram.user_limit_day, 60 * 60 * 24, BucketType.user) + +# Allows for max api requests / days in month per day for the entire guild (Temporary) +guildcd = commands.CooldownMapping.from_cooldown(Wolfram.guild_limit_day, 60 * 60 * 24, BucketType.guild) + + +async def send_embed( +        ctx: Context, +        message_txt: str, +        colour: int = Colours.soft_red, +        footer: str = None, +        img_url: str = None, +        f: discord.File = None +) -> None: +    """Generate & send a response embed with Wolfram as the author.""" +    embed = Embed(colour=colour) +    embed.description = message_txt +    embed.set_author(name="Wolfram Alpha", +                     icon_url=WOLF_IMAGE, +                     url="https://www.wolframalpha.com/") +    if footer: +        embed.set_footer(text=footer) + +    if img_url: +        embed.set_image(url=img_url) + +    await ctx.send(embed=embed, file=f) + + +def custom_cooldown(*ignore: List[int]) -> Callable: +    """ +    Implement per-user and per-guild cooldowns for requests to the Wolfram API. + +    A list of roles may be provided to ignore the per-user cooldown +    """ +    async def predicate(ctx: Context) -> bool: +        if ctx.invoked_with == 'help': +            # if the invoked command is help we don't want to increase the ratelimits since it's not actually +            # invoking the command/making a request, so instead just check if the user/guild are on cooldown. +            guild_cooldown = not guildcd.get_bucket(ctx.message).get_tokens() == 0  # if guild is on cooldown +            if not any(r.id in ignore for r in ctx.author.roles):  # check user bucket if user is not ignored +                return guild_cooldown and not usercd.get_bucket(ctx.message).get_tokens() == 0 +            return guild_cooldown + +        user_bucket = usercd.get_bucket(ctx.message) + +        if all(role.id not in ignore for role in ctx.author.roles): +            user_rate = user_bucket.update_rate_limit() + +            if user_rate: +                # Can't use api; cause: member limit +                cooldown = arrow.utcnow().shift(seconds=int(user_rate)).humanize(only_distance=True) +                message = ( +                    "You've used up your limit for Wolfram|Alpha requests.\n" +                    f"Cooldown: {cooldown}" +                ) +                await send_embed(ctx, message) +                return False + +        guild_bucket = guildcd.get_bucket(ctx.message) +        guild_rate = guild_bucket.update_rate_limit() + +        # Repr has a token attribute to read requests left +        log.debug(guild_bucket) + +        if guild_rate: +            # Can't use api; cause: guild limit +            message = ( +                "The max limit of requests for the server has been reached for today.\n" +                f"Cooldown: {int(guild_rate)}" +            ) +            await send_embed(ctx, message) +            return False + +        return True + +    return check(predicate) + + +async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional[List[Tuple]]: +    """Get the Wolfram API pod pages for the provided query.""" +    async with ctx.channel.typing(): +        url_str = parse.urlencode({ +            "input": query, +            "appid": APPID, +            "output": DEFAULT_OUTPUT_FORMAT, +            "format": "image,plaintext" +        }) +        request_url = QUERY.format(request="query", data=url_str) + +        async with bot.http_session.get(request_url) as response: +            json = await response.json(content_type='text/plain') + +        result = json["queryresult"] + +        if result["error"]: +            # API key not set up correctly +            if result["error"]["msg"] == "Invalid appid": +                message = "Wolfram API key is invalid or missing." +                log.warning( +                    "API key seems to be missing, or invalid when " +                    f"processing a wolfram request: {url_str}, Response: {json}" +                ) +                await send_embed(ctx, message) +                return + +            message = "Something went wrong internally with your request, please notify staff!" +            log.warning(f"Something went wrong getting a response from wolfram: {url_str}, Response: {json}") +            await send_embed(ctx, message) +            return + +        if not result["success"]: +            message = f"I couldn't find anything for {query}." +            await send_embed(ctx, message) +            return + +        if not result["numpods"]: +            message = "Could not find any results." +            await send_embed(ctx, message) +            return + +        pods = result["pods"] +        pages = [] +        for pod in pods[:MAX_PODS]: +            subs = pod.get("subpods") + +            for sub in subs: +                title = sub.get("title") or sub.get("plaintext") or sub.get("id", "") +                img = sub["img"]["src"] +                pages.append((title, img)) +        return pages + + +class Wolfram(Cog): +    """Commands for interacting with the Wolfram|Alpha API.""" + +    def __init__(self, bot: commands.Bot): +        self.bot = bot + +    @group(name="wolfram", aliases=("wolf", "wa"), invoke_without_command=True) +    @custom_cooldown(*STAFF_ROLES) +    async def wolfram_command(self, ctx: Context, *, query: str) -> None: +        """Requests all answers on a single image, sends an image of all related pods.""" +        url_str = parse.urlencode({ +            "i": query, +            "appid": APPID, +        }) +        query = QUERY.format(request="simple", data=url_str) + +        # Give feedback that the bot is working. +        async with ctx.channel.typing(): +            async with self.bot.http_session.get(query) as response: +                status = response.status +                image_bytes = await response.read() + +            f = discord.File(BytesIO(image_bytes), filename="image.png") +            image_url = "attachment://image.png" + +            if status == 501: +                message = "Failed to get response" +                footer = "" +                color = Colours.soft_red +            elif status == 400: +                message = "No input found" +                footer = "" +                color = Colours.soft_red +            elif status == 403: +                message = "Wolfram API key is invalid or missing." +                footer = "" +                color = Colours.soft_red +            else: +                message = "" +                footer = "View original for a bigger picture." +                color = Colours.soft_orange + +            # Sends a "blank" embed if no request is received, unsure how to fix +            await send_embed(ctx, message, color, footer=footer, img_url=image_url, f=f) + +    @wolfram_command.command(name="page", aliases=("pa", "p")) +    @custom_cooldown(*STAFF_ROLES) +    async def wolfram_page_command(self, ctx: Context, *, query: str) -> None: +        """ +        Requests a drawn image of given query. + +        Keywords worth noting are, "like curve", "curve", "graph", "pokemon", etc. +        """ +        pages = await get_pod_pages(ctx, self.bot, query) + +        if not pages: +            return + +        embed = Embed() +        embed.set_author(name="Wolfram Alpha", +                         icon_url=WOLF_IMAGE, +                         url="https://www.wolframalpha.com/") +        embed.colour = Colours.soft_orange + +        await ImagePaginator.paginate(pages, ctx, embed) + +    @wolfram_command.command(name="cut", aliases=("c",)) +    @custom_cooldown(*STAFF_ROLES) +    async def wolfram_cut_command(self, ctx: Context, *, query: str) -> None: +        """ +        Requests a drawn image of given query. + +        Keywords worth noting are, "like curve", "curve", "graph", "pokemon", etc. +        """ +        pages = await get_pod_pages(ctx, self.bot, query) + +        if not pages: +            return + +        if len(pages) >= 2: +            page = pages[1] +        else: +            page = pages[0] + +        await send_embed(ctx, page[0], colour=Colours.soft_orange, img_url=page[1]) + +    @wolfram_command.command(name="short", aliases=("sh", "s")) +    @custom_cooldown(*STAFF_ROLES) +    async def wolfram_short_command(self, ctx: Context, *, query: str) -> None: +        """Requests an answer to a simple question.""" +        url_str = parse.urlencode({ +            "i": query, +            "appid": APPID, +        }) +        query = QUERY.format(request="result", data=url_str) + +        # Give feedback that the bot is working. +        async with ctx.channel.typing(): +            async with self.bot.http_session.get(query) as response: +                status = response.status +                response_text = await response.text() + +            if status == 501: +                message = "Failed to get response" +                color = Colours.soft_red +            elif status == 400: +                message = "No input found" +                color = Colours.soft_red +            elif response_text == "Error 1: Invalid appid": +                message = "Wolfram API key is invalid or missing." +                color = Colours.soft_red +            else: +                message = response_text +                color = Colours.soft_orange + +            await send_embed(ctx, message, color) + + +def setup(bot: commands.Bot) -> None: +    """Load the Wolfram cog.""" +    bot.add_cog(Wolfram(bot)) diff --git a/bot/resources/easter/starter.json b/bot/resources/easter/starter.json deleted file mode 100644 index 31e2cbc9..00000000 --- a/bot/resources/easter/starter.json +++ /dev/null @@ -1,24 +0,0 @@ -{ -  "starters": [ -    "What is your favourite Easter candy or treat?", -    "What is your earliest memory of Easter?", -    "What is the title of the last book you read?", -    "What is better: Milk, Dark or White chocolate?", -    "What is your favourite holiday?", -    "If you could have any superpower, what would it be?", -    "Name one thing you like about a person to your right.", -    "If you could be anyone else for one day, who would it be?", -    "What Easter tradition do you enjoy most?", -    "What is the best gift you've been given?", -    "Name one famous person you would like to have at your easter dinner.", -    "What was the last movie you saw in a cinema?", -    "What is your favourite food?", -    "If you could travel anywhere in the world, where would you go?", -    "Tell us 5 things you do well.", -    "What is your favourite place that you have visited?", -    "What is your favourite color?", -    "If you had $100 bill in your Easter Basket, what would you do with it?", -    "What would you do if you know you could succeed at anything you chose to do?", -    "If you could take only three things from your house, what would they be?" -  ] -} diff --git a/bot/resources/evergreen/caesar_info.json b/bot/resources/evergreen/caesar_info.json new file mode 100644 index 00000000..8229c4f3 --- /dev/null +++ b/bot/resources/evergreen/caesar_info.json @@ -0,0 +1,4 @@ +{ +    "title": "Caesar Cipher", +    "description": "**Information**\nThe Caesar Cipher, named after the Roman General Julius Caesar, is one of the simplest and most widely known encryption techniques. It is a type of substitution cipher in which each letter in the plaintext is replaced by a letter given a specific position offset in the alphabet, with the letters wrapping around both sides.\n\n**Examples**\n1) `Hello World` <=> `Khoor Zruog` where letters are shifted forwards by `3`.\n2) `Julius Caesar` <=> `Yjaxjh Rpthpg` where letters are shifted backwards by `11`." +} diff --git a/bot/resources/evergreen/py_topics.yaml b/bot/resources/evergreen/py_topics.yaml new file mode 100644 index 00000000..1e53429a --- /dev/null +++ b/bot/resources/evergreen/py_topics.yaml @@ -0,0 +1,89 @@ +# Conversation starters for Python-related channels. + +# python-general +267624335836053506: +    - What's your favorite PEP? +    - What's your current text editor/IDE, and what functionality do you like about it the most when programming in Python? +    - What functionality is your text editor/IDE missing for programming Python? +    - What parts of your life has Python automated, if any? +    - Which Python project are you the most proud of making? +    - What made you want to learn Python? +    - When did you start learning Python? +    - What reasons are you learning Python for? +    - Where's the strangest place you've seen Python? +    - How has learning Python changed your life? +    - Is there a package you wish existed but doesn't? What is it? +    - What feature do you think should be added to Python? +    - Has Python helped you in school? If so, how? +    - What was the first thing you created with Python? + +# async +630504881542791169: +    - Are there any frameworks you wish were async? +    - How have coroutines changed the way you write Python? + +# c-extensions +728390945384431688: +    - + +# computer-science +650401909852864553: +    - + +# databases +342318764227821568: +    - Where do you get your best data? + +# data-science +366673247892275221: +    - + +# discord.py +343944376055103488: +    - What unique features does your bot contain, if any? +    - What commands/features are you proud of making? +    - What feature would you be the most interested in making? +    - What feature would you like to see added to the library? what feature in the library do you think is redundant? +    - Do you think there's a way in which Discord could handle bots better? + +# esoteric-python +470884583684964352: +    - What's a common part of programming we can make harder? +    - What are the pros and cons of messing with __magic__()? + +# game-development +660625198390837248: +    - + +# microcontrollers +545603026732318730: +    - + +# networking +716325106619777044: +    - If you could wish for a library involving networking, what would it be? + +# security +366674035876167691: +    - If you could wish for a library involving net-sec, what would it be? + +# software-testing +463035728335732738: +    - + +# tools-and-devops +463035462760792066: +    - What editor would you recommend to a beginner? Why? +    - What editor would you recommend to be the most efficient? Why? + +# unix +491523972836360192: +    - + +# user-interfaces +338993628049571840: +    - What's the most impressive Desktop Application you've made with Python so far? + +# web-development +366673702533988363: +    - How has Python helped you in web development? diff --git a/bot/resources/evergreen/starter.yaml b/bot/resources/evergreen/starter.yaml new file mode 100644 index 00000000..53c89364 --- /dev/null +++ b/bot/resources/evergreen/starter.yaml @@ -0,0 +1,22 @@ +# Conversation starters for channels that are not Python-related. + +- What is your favourite Easter candy or treat? +- What is your earliest memory of Easter? +- What is the title of the last book you read? +- "What is better: Milk, Dark or White chocolate?" +- What is your favourite holiday? +- If you could have any superpower, what would it be? +- Name one thing you like about a person to your right. +- If you could be anyone else for one day, who would it be? +- What Easter tradition do you enjoy most? +- What is the best gift you've been given? +- Name one famous person you would like to have at your easter dinner. +- What was the last movie you saw in a cinema? +- What is your favourite food? +- If you could travel anywhere in the world, where would you go? +- Tell us 5 things you do well. +- What is your favourite place that you have visited? +- What is your favourite color? +- If you had $100 bill in your Easter Basket, what would you do with it? +- What would you do if you know you could succeed at anything you chose to do? +- If you could take only three things from your house, what would they be? diff --git a/bot/resources/evergreen/trivia_quiz.json b/bot/resources/evergreen/trivia_quiz.json index 6100ca62..8f0a4114 100644 --- a/bot/resources/evergreen/trivia_quiz.json +++ b/bot/resources/evergreen/trivia_quiz.json @@ -217,6 +217,36 @@        "question": "What does the acronym GPRS stand for?",        "answer": "General Packet Radio Service",        "info": "General Packet Radio Service (GPRS) is a packet-based mobile data service on the global system for mobile communications (GSM) of 3G and 2G cellular communication systems. It is a non-voice, high-speed and useful packet-switching technology intended for GSM networks." +    }, +    { +      "id": 131, +      "question": "In what country is the Ebro river located?", +      "answer": "Spain", +      "info": "The Ebro river is located in Spain. It is 930 kilometers long and it's the second longest river that ends on the Mediterranean Sea." +    }, +    { +      "id": 132, +      "question": "What year was the IBM PC model 5150 introduced into the market?", +      "answer": "1981", +      "info": "The IBM PC was introduced into the market in 1981. It used the Intel 8088, with a clock speed of 4.77 MHz, along with the MDA and CGA as a video card." +    }, +    { +      "id": 133, +      "question": "What's the world's largest urban area?", +      "answer": "Tokyo", +      "info": "Tokyo is the most populated city in the world, with a population of 37 million people. It is located in Japan." +    }, +    { +      "id": 134, +      "question": "How many planets are there in the Solar system?", +      "answer": "8", +      "info": "In the Solar system, there are 8 planets: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus and Neptune. Pluto isn't considered a planet in the Solar System anymore." +    }, +    { +      "id": 135, +      "question": "What is the capital of Iraq?", +      "answer": "Baghdad", +      "info": "Baghdad is the capital of Iraq. It has a population of 7 million people."      }    ]  } diff --git a/bot/utils/randomization.py b/bot/utils/randomization.py new file mode 100644 index 00000000..8f47679a --- /dev/null +++ b/bot/utils/randomization.py @@ -0,0 +1,23 @@ +import itertools +import random +import typing as t + + +class RandomCycle: +    """ +    Cycles through elements from a randomly shuffled iterable, repeating indefinitely. + +    The iterable is reshuffled after each full cycle. +    """ + +    def __init__(self, iterable: t.Iterable) -> None: +        self.iterable = list(iterable) +        self.index = itertools.cycle(range(len(iterable))) + +    def __next__(self) -> t.Any: +        idx = next(self.index) + +        if idx == 0: +            random.shuffle(self.iterable) + +        return self.iterable[idx] | 
