aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar momothereal <[email protected]>2018-07-25 01:11:55 -0400
committerGravatar momothereal <[email protected]>2018-07-25 01:11:55 -0400
commit2cf56837693fd0ea14181ac52a0b6080296a9223 (patch)
tree01acb9cdd1d8cd696579fd42c39d18ba6f9b221b
parentAdd 'edit reason' command (diff)
parentAttempt to speed up build (diff)
Merge branch 'master' into feature/rowboat-replacement
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--Pipfile1
-rw-r--r--Pipfile.lock315
-rw-r--r--bot/__main__.py5
-rw-r--r--bot/cogs/bigbrother.py182
-rw-r--r--bot/cogs/bot.py10
-rw-r--r--bot/cogs/cogs.py8
-rw-r--r--bot/cogs/doc.py11
-rw-r--r--bot/cogs/modlog.py658
-rw-r--r--bot/cogs/snekbox.py50
-rw-r--r--bot/cogs/verification.py6
-rw-r--r--bot/constants.py88
-rw-r--r--bot/utils/time.py59
-rw-r--r--config-default.yml158
14 files changed, 1302 insertions, 251 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ea398d508..88ab5d927 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -11,7 +11,7 @@ test:
stage: test
script:
- - pipenv sync --dev
+ - pipenv install --dev --deploy
- pipenv run lint
build:
diff --git a/Pipfile b/Pipfile
index 9b0e530ec..d94234b22 100644
--- a/Pipfile
+++ b/Pipfile
@@ -22,6 +22,7 @@ python-levenshtein = "*"
pillow = "*"
aio-pika = "*"
python-dateutil = "*"
+deepdiff = "*"
[dev-packages]
"flake8" = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index 9f4c3a1d4..864eb574a 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "2475ab039e5ce97636b9a255dd7e21b67e7b66d832f341b4065876f7535b2d88"
+ "sha256": "c7d1bad1549c322484f6751447115ded9299df039cb6321bcc1a1fa1359481dc"
},
"pipfile-spec": 6,
"requires": {
@@ -18,11 +18,11 @@
"default": {
"aio-pika": {
"hashes": [
- "sha256:2e44d888956bf64e3d1edd624c02279a19faed3dc0cc3ff2d5af03f4fe15b18a",
- "sha256:c453f233fb734daeb15c7191d7e5aa20a5c5c317d6d2c26607cb419c807fffd8"
+ "sha256:c19c38155f4972f6a9f3f0f1095ce261bfb4e8b89553ead240486593aafd9431",
+ "sha256:d41748994e2f809c440a04a1eb809aaae00691caa8e2dab7376d640131754aa4"
],
"index": "pypi",
- "version": "==2.8.3"
+ "version": "==3.0.1"
},
"aiodns": {
"hashes": [
@@ -53,10 +53,10 @@
},
"alabaster": {
"hashes": [
- "sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732",
- "sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0"
+ "sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456",
+ "sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"
],
- "version": "==0.7.10"
+ "version": "==0.7.11"
},
"async-timeout": {
"hashes": [
@@ -94,6 +94,15 @@
],
"version": "==3.0.4"
},
+ "deepdiff": {
+ "hashes": [
+ "sha256:152b29dd9cd97cc78403121fb394925ec47377d4a410751e56547c3930ba2b39",
+ "sha256:b4150052e610b231885c4c0be3eea86e4c029df91550ec51b9fc14dd209a5055",
+ "sha256:ecad8e16a96ffd27e8f40c9801a6ab16ec6a7e7e6e6859a7710ba4695f22702c"
+ ],
+ "index": "pypi",
+ "version": "==3.3.0"
+ },
"discord": {
"egg": "discord.py[voice]",
"file": "https://github.com/Rapptz/discord.py/archive/rewrite.zip"
@@ -108,10 +117,10 @@
},
"dulwich": {
"hashes": [
- "sha256:c51e10c260543240e0806052af046e1a78b98cbe1ac1ef3880a78d2269e09da4"
+ "sha256:34f99e575fe1f1e89cca92cec1ddd50b4991199cb00609203b28df9eb83ce259"
],
"index": "pypi",
- "version": "==0.19.2"
+ "version": "==0.19.5"
},
"fuzzywuzzy": {
"hashes": [
@@ -123,10 +132,10 @@
},
"idna": {
"hashes": [
- "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
- "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
+ "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
+ "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
- "version": "==2.6"
+ "version": "==2.7"
},
"imagesize": {
"hashes": [
@@ -142,6 +151,12 @@
],
"version": "==2.10"
},
+ "jsonpickle": {
+ "hashes": [
+ "sha256:545b3bee0d65e1abb4baa1818edcc9ec239aa9f2ffbfde8084d71c056180054f"
+ ],
+ "version": "==0.9.6"
+ },
"logmatic-python": {
"hashes": [
"sha256:0c15ac9f5faa6a60059b28910db642c3dc7722948c3cc940923f8c9039604342"
@@ -151,37 +166,35 @@
},
"lxml": {
"hashes": [
- "sha256:01c45df6d90497c20aa2a07789a41941f9a1029faa30bf725fc7f6d515b1afe9",
- "sha256:0c9fef4f8d444e337df96c54544aeb85b7215b2ed7483bb6c35de97ac99f1bcd",
- "sha256:0e3cd94c95d30ba9ca3cff40e9b2a14e1a10a4fd8131105b86c6b61648f57e4b",
- "sha256:0e7996e9b46b4d8b4ac1c329a00e2d10edcd8380b95d2a676fccabf4c1dd0512",
- "sha256:1858b1933d483ec5727549d3fe166eeb54229fbd6a9d3d7ea26d2c8a28048058",
- "sha256:1b164bba1320b14905dcff77da10d5ce9c411ac4acc4fb4ed9a2a4d10fae38c9",
- "sha256:1b46f37927fa6cd1f3fe34b54f1a23bd5bea1d905657289e08e1297069a1a597",
- "sha256:231047b05907315ae9a9b6925751f9fd2c479cf7b100fff62485a25e382ca0d4",
- "sha256:28f0c6652c1b130f1e576b60532f84b19379485eb8da6185c29bd8c9c9bc97bf",
- "sha256:34d49d0f72dd82b9530322c48b70ac78cca0911275da741c3b1d2f3603c5f295",
- "sha256:3682a17fbf72d56d7e46db2e80ca23850b79c28cfe75dcd9b82f58808f730909",
- "sha256:3cf2830b9a6ad7f6e965fa53a768d4d2372a7856f20ffa6ce43d2fe9c0d34b19",
- "sha256:5b653c9379ce29ce271fbe1010c5396670f018e78b643e21beefbb3dc6d291de",
- "sha256:65a272821d5d8194358d6b46f3ca727fa56a6b63981606eac737c86d27309cdd",
- "sha256:691f2cd97cf026c611df1ea5055755eec7f878f2d4f4330dc8686583de6fc5fd",
- "sha256:6b6379495d3baacf7ed755ac68547c8dff6ce5d37bf370f0b7678888dc1283f9",
- "sha256:75322a531504d4f383264391d89993a42e286da8821ddc5ac315e57305cb84f0",
- "sha256:7f457cbda964257f443bac861d3a36732dcba8183149e7818ee2fb7c86901b94",
- "sha256:7ff1fc76d8804e0f870c343a72007ff587090c218b0f92d8ee784ac2b6eaf5b9",
- "sha256:8523fbde9c2216f3f2b950cb01ebe52e785eaa8a07ffeb456dd3576ca1b4fb9b",
- "sha256:8f37627f16e026523fca326f1b5c9a43534862fede6c3e99c2ba6a776d75c1ab",
- "sha256:a7182ea298cc3555ea56ffbb0748fe0d5e0d81451e2bc16d7f4645cd01b1ca70",
- "sha256:abbd2fb4a5a04c11b5e04eb146659a0cf67bb237dd3d7ca3b9994d3a9f826e55",
- "sha256:accc9f6b77bed0a6f267b4fae120f6008a951193d548cdbe9b61fc98a08b1cf8",
- "sha256:bd88c8ce0d1504fdfd96a35911dd4f3edfb2e560d7cfdb5a3d09aa571ae5fbae",
- "sha256:c557ad647facb3c0027a9d0af58853f905e85a0a2f04dcb73f8e665272fcdc3a",
- "sha256:defabb7fbb99f9f7b3e0b24b286a46855caef4776495211b066e9e6592d12b04",
- "sha256:e2629cdbcad82b83922a3488937632a4983ecc0fed3e5cfbf430d069382eeb9b"
+ "sha256:0941f4313208c07734410414d8308812b044fd3fb98573454e3d3a0d2e201f3d",
+ "sha256:0b18890aa5730f9d847bc5469e8820f782d72af9985a15a7552109a86b01c113",
+ "sha256:21f427945f612ac75576632b1bb8c21233393c961f2da890d7be3927a4b6085f",
+ "sha256:24cf6f622a4d49851afcf63ac4f0f3419754d4e98a7a548ab48dd03c635d9bd3",
+ "sha256:2dc6705486b8abee1af9e2a3761e30a3cb19e8276f20ca7e137ee6611b93707c",
+ "sha256:2e43b2e5b7d2b9abe6e0301eef2c2c122ab45152b968910eae68bdee2c4cfae0",
+ "sha256:329a6d8b6d36f7d6f8b6c6a1db3b2c40f7e30a19d3caf62023c9d6a677c1b5e1",
+ "sha256:423cde55430a348bda6f1021faad7235c2a95a6bdb749e34824e5758f755817a",
+ "sha256:4651ea05939374cfb5fe87aab5271ed38c31ea47997e17ec3834b75b94bd9f15",
+ "sha256:4be3bbfb2968d7da6e5c2cd4104fc5ec1caf9c0794f6cae724da5a53b4d9f5a3",
+ "sha256:622f7e40faef13d232fb52003661f2764ce6cdef3edb0a59af7c1559e4cc36d1",
+ "sha256:664dfd4384d886b239ef0d7ee5cff2b463831079d250528b10e394a322f141f9",
+ "sha256:697c0f58ac637b11991a1bc92e07c34da4a72e2eda34d317d2c1c47e2f24c1b3",
+ "sha256:6ec908b4c8a4faa7fe1a0080768e2ce733f268b287dfefb723273fb34141475f",
+ "sha256:7ec3fe795582b75bb49bb1685ffc462dbe38d74312dac07ce386671a28b5316b",
+ "sha256:8c39babd923c431dcf1e5874c0f778d3a5c745a62c3a9b6bd755efd489ee8a1d",
+ "sha256:949ca5bc56d6cb73d956f4862ba06ad3c5d2808eac76304284f53ae0c8b2334a",
+ "sha256:9f0daddeefb0791a600e6195441910bdf01eac470be596b9467e6122b51239a6",
+ "sha256:a359893b01c30e949eae0e8a85671a593364c9f0b8162afe0cb97317af0953bf",
+ "sha256:ad5d5d8efed59e6b1d4c50c1eac59fb6ecec91b2073676af1e15fc4d43e9b6c5",
+ "sha256:bc1a36f95a6b3667c09b34995fc3a46a82e4cf0dc3e7ab281e4c77b15bd7af05",
+ "sha256:be37b3f55b6d7d923f43bf74c356fc1878eb36e28505f38e198cb432c19c7b1a",
+ "sha256:c45bca5e544eb75f7500ffd730df72922eb878a2f0213b0dc5a5f357ded3a85d",
+ "sha256:ccee7ebbb4735ebc341d347fca9ee09f2fa6c0580528c1414bc4e1d31372835c",
+ "sha256:dc62c0840b2fc7753550b40405532a3e125c0d3761f34af948873393aa688160",
+ "sha256:f7d9d5aa1c7e54167f1a3cba36b5c52c7c540f30952c9bd7d9302a1eda318424"
],
"index": "pypi",
- "version": "==4.2.1"
+ "version": "==4.2.3"
},
"markdownify": {
"hashes": [
@@ -228,61 +241,41 @@
],
"version": "==17.1"
},
- "pika": {
- "hashes": [
- "sha256:63131aaeec48a6c8f1db1fe657e1e74cf384c3927eb7d1725e31edae4220dea4",
- "sha256:7277b4d12a99efa4058782614d84138983f9f89d690bdfcea66290d810806459"
- ],
- "version": "==0.10.0"
- },
"pillow": {
"hashes": [
- "sha256:00633bc2ec40313f4daf351855e506d296ec3c553f21b66720d0f1225ca84c6f",
- "sha256:03514478db61b034fc5d38b9bf060f994e5916776e93f02e59732a8270069c61",
- "sha256:040144ba422216aecf7577484865ade90e1a475f867301c48bf9fbd7579efd76",
- "sha256:16246261ff22368e5e32ad74d5ef40403ab6895171a7fc6d34f6c17cfc0f1943",
- "sha256:1cb38df69362af35c14d4a50123b63c7ff18ec9a6d4d5da629a6f19d05e16ba8",
- "sha256:2400e122f7b21d9801798207e424cbe1f716cee7314cd0c8963fdb6fc564b5fb",
- "sha256:2ee6364b270b56a49e8b8a51488e847ab130adc1220c171bed6818c0d4742455",
- "sha256:3b4560c3891b05022c464b09121bd507c477505a4e19d703e1027a3a7c68d896",
- "sha256:41374a6afb3f44794410dab54a0d7175e6209a5a02d407119c81083f1a4c1841",
- "sha256:438a3faf5f702c8d0f80b9f9f9b8382cfa048ca6a0d64ef71b86b563b0ee0359",
- "sha256:472a124c640bde4d5468f6991c9fa7e30b723d84ac4195a77c6ab6aea30f2b9c",
- "sha256:4d32c8e3623a61d6e29ccd024066cd1ba556555abfb4cd714155020e00107e3f",
- "sha256:4d8077fd649ac40a5c4165f2c22fa2a4ad18c668e271ecb2f9d849d1017a9313",
- "sha256:62ec7ae98357fcd46002c110bb7cad15fce532776f0cbe7ca1d44c49b837d49d",
- "sha256:6c7cab6a05351cf61e469937c49dbf3cdf5ffb3eeac71f8d22dc9be3507598d8",
- "sha256:6eca36905444c4b91fe61f1b9933a47a30480738a1dd26501ff67d94fc2bc112",
- "sha256:74e2ebfd19c16c28ad43b8a28ff73b904ed382ea4875188838541751986e8c9a",
- "sha256:7673e7473a13107059377c96c563aa36f73184c29d2926882e0a0210b779a1e7",
- "sha256:81762cf5fca9a82b53b7b2d0e6b420e0f3b06167b97678c81d00470daa622d58",
- "sha256:8554bbeb4218d9cfb1917c69e6f2d2ad0be9b18a775d2162547edf992e1f5f1f",
- "sha256:9b66e968da9c4393f5795285528bc862c7b97b91251f31a08004a3c626d18114",
- "sha256:a00edb2dec0035e98ac3ec768086f0b06dfabb4ad308592ede364ef573692f55",
- "sha256:b48401752496757e95304a46213c3155bc911ac884bed2e9b275ce1c1df3e293",
- "sha256:b6cf18f9e653a8077522bb3aa753a776b117e3e0cc872c25811cfdf1459491c2",
- "sha256:bb8adab1877e9213385cbb1adc297ed8337e01872c42a30cfaa66ff8c422779c",
- "sha256:c8a4b39ba380b57a31a4b5449a9d257b1302d8bc4799767e645dcee25725efe1",
- "sha256:cee9bc75bff455d317b6947081df0824a8f118de2786dc3d74a3503fd631f4ef",
- "sha256:d0dc1313dff48af64517cbbd85e046d6b477fbe5e9d69712801f024dcb08c62b",
- "sha256:d5bf527ed83617edd1855a5c923eeeaf68bcb9ac0ceb28e3f19b575b3a424984",
- "sha256:df5863a21f91de5ecdf7d32a32f406dd9867ebb35d41033b8bd9607a21887599",
- "sha256:e39142332541ed2884c257495504858b22c078a5d781059b07aba4c3a80d7551",
- "sha256:e52e8f675ba0b2b417fa98579e7286a41a8e23871f17f4793772f5aa884fea79",
- "sha256:e6dd55d5d94b9e36929325dd0c9ab85bfde84a5fc35947c334c32af1af668944",
- "sha256:e87cc1acbebf263f308a8494272c2d42016aa33c32bf14d209c81e1f65e11868",
- "sha256:ea0091cd4100519cedfeea2c659f52291f535ac6725e2368bcf59e874f270efa",
- "sha256:eeb247f4f4d962942b3b555530b0c63b77473c7bfe475e51c6b75b7344b49ce3",
- "sha256:f0d4433adce6075efd24fc0285135248b0b50f5a58129c7e552030e04fe45c7f",
- "sha256:f1f3bd92f8e12dc22884935a73c9f94c4d9bd0d34410c456540713d6b7832b8c",
- "sha256:f42a87cbf50e905f49f053c0b1fb86c911c730624022bf44c8857244fc4cdaca",
- "sha256:f5f302db65e2e0ae96e26670818157640d3ca83a3054c290eff3631598dcf819",
- "sha256:f7634d534662bbb08976db801ba27a112aee23e597eeaf09267b4575341e45bf",
- "sha256:fdd374c02e8bb2d6468a85be50ea66e1c4ef9e809974c30d8576728473a6ed03",
- "sha256:fe6931db24716a0845bd8c8915bd096b77c2a7043e6fc59ae9ca364fe816f08b"
+ "sha256:00def5b638994f888d1058e4d17c86dec8e1113c3741a0a8a659039aec59a83a",
+ "sha256:026449b64e559226cdb8e6d8c931b5965d8fc90ec18ebbb0baa04c5b36503c72",
+ "sha256:03dbb224ee196ef30ed2156d41b579143e1efeb422974719a5392fc035e4f574",
+ "sha256:03eb0e04f929c102ae24bc436bf1c0c60a4e63b07ebd388e84d8b219df3e6acd",
+ "sha256:1be66b9a89e367e7d20d6cae419794997921fe105090fafd86ef39e20a3baab2",
+ "sha256:1e977a3ed998a599bda5021fb2c2889060617627d3ae228297a529a082a3cd5c",
+ "sha256:22cf3406d135cfcc13ec6228ade774c8461e125c940e80455f500638429be273",
+ "sha256:24adccf1e834f82718c7fc8e3ec1093738da95144b8b1e44c99d5fc7d3e9c554",
+ "sha256:2a3e362c97a5e6a259ee9cd66553292a1f8928a5bdfa3622fdb1501570834612",
+ "sha256:3832e26ecbc9d8a500821e3a1d3765bda99d04ae29ffbb2efba49f5f788dc934",
+ "sha256:4fd1f0c2dc02aaec729d91c92cd85a2df0289d88e9f68d1e8faba750bb9c4786",
+ "sha256:4fda62030f2c515b6e2e673c57caa55cb04026a81968f3128aae10fc28e5cc27",
+ "sha256:5044d75a68b49ce36a813c82d8201384207112d5d81643937fc758c05302f05b",
+ "sha256:522184556921512ec484cb93bd84e0bab915d0ac5a372d49571c241a7f73db62",
+ "sha256:5914cff11f3e920626da48e564be6818831713a3087586302444b9c70e8552d9",
+ "sha256:6661a7908d68c4a133e03dac8178287aa20a99f841ea90beeb98a233ae3fd710",
+ "sha256:79258a8df3e309a54c7ef2ef4a59bb8e28f7e4a8992a3ad17c24b1889ced44f3",
+ "sha256:7d74c20b8f1c3e99d3f781d3b8ff5abfefdd7363d61e23bdeba9992ff32cc4b4",
+ "sha256:81918afeafc16ba5d9d0d4e9445905f21aac969a4ebb6f2bff4b9886da100f4b",
+ "sha256:8194d913ca1f459377c8a4ed8f9b7ad750068b8e0e3f3f9c6963fcc87a84515f",
+ "sha256:84d5d31200b11b3c76fab853b89ac898bf2d05c8b3da07c1fcc23feb06359d6e",
+ "sha256:989981db57abffb52026b114c9a1f114c7142860a6d30a352d28f8cbf186500b",
+ "sha256:a3d7511d3fad1618a82299aab71a5fceee5c015653a77ffea75ced9ef917e71a",
+ "sha256:b3ef168d4d6fd4fa6685aef7c91400f59f7ab1c0da734541f7031699741fb23f",
+ "sha256:c1c5792b6e74bbf2af0f8e892272c2a6c48efa895903211f11b8342e03129fea",
+ "sha256:c5dcb5a56aebb8a8f2585042b2f5c496d7624f0bcfe248f0cc33ceb2fd8d39e7",
+ "sha256:e2bed4a04e2ca1050bb5f00865cf2f83c0b92fd62454d9244f690fcd842e27a4",
+ "sha256:e87a527c06319428007e8c30511e1f0ce035cb7f14bb4793b003ed532c3b9333",
+ "sha256:f63e420180cbe22ff6e32558b612e75f50616fc111c5e095a4631946c782e109",
+ "sha256:f8b3d413c5a8f84b12cd4c5df1d8e211777c9852c6be3ee9c094b626644d3eab"
],
"index": "pypi",
- "version": "==5.1.0"
+ "version": "==5.2.0"
},
"pycares": {
"hashes": [
@@ -320,11 +313,6 @@
"pyparsing": {
"hashes": [
"sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
- "sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07",
- "sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18",
- "sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e",
- "sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5",
- "sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58",
"sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
],
"version": "==2.2.0"
@@ -339,10 +327,11 @@
},
"python-json-logger": {
"hashes": [
- "sha256:30999d1d742ecf6645991a2ce9273188505e98b713ad63be06aabff47dd1b3c4",
- "sha256:8205cfe7061715de5cd1b37e3565d5b97d0ac13b30ff3ee612554abb6093d640"
+ "sha256:a292e22c5e03105a05a746ade6209d43db1c4c763b91c75c8486e81d10904d85",
+ "sha256:e3636824d35ba6a15fc39f573588cba63cf46322a5dc86fb2f280229077e9fbe"
],
- "version": "==0.1.8"
+ "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
+ "version": "==0.1.9"
},
"python-levenshtein": {
"hashes": [
@@ -353,37 +342,34 @@
},
"pytz": {
"hashes": [
- "sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555",
- "sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749"
+ "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
+ "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
],
- "version": "==2018.4"
+ "version": "==2018.5"
},
"pyyaml": {
"hashes": [
- "sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8",
- "sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736",
- "sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f",
- "sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608",
- "sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8",
- "sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab",
- "sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7",
- "sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3",
- "sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1",
- "sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6",
- "sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8",
- "sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
- "sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
- "sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
+ "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
+ "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
+ "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
+ "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
+ "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
+ "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
+ "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
+ "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
+ "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
+ "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
+ "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
],
"index": "pypi",
- "version": "==3.12"
+ "version": "==3.13"
},
"requests": {
"hashes": [
- "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
- "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
+ "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
+ "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
],
- "version": "==2.18.4"
+ "version": "==2.19.1"
},
"shortuuid": {
"hashes": [
@@ -407,32 +393,34 @@
},
"sphinx": {
"hashes": [
- "sha256:85f7e32c8ef07f4ba5aeca728e0f7717bef0789fba8458b8d9c5c294cad134f3",
- "sha256:d45480a229edf70d84ca9fae3784162b1bc75ee47e480ffe04a4b7f21a95d76d"
+ "sha256:217ad9ece2156ed9f8af12b5d2c82a499ddf2c70a33c5f81864a08d8c67b9efc",
+ "sha256:a765c6db1e5b62aae857697cd4402a5c1a315a7b0854bbcd0fc8cdc524da5896"
],
"index": "pypi",
- "version": "==1.7.5"
+ "version": "==1.7.6"
},
"sphinxcontrib-websupport": {
"hashes": [
- "sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9",
- "sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2"
+ "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
+ "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
],
- "version": "==1.0.1"
+ "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
+ "version": "==1.1.0"
},
"sympy": {
"hashes": [
- "sha256:ac5b57691bc43919dcc21167660a57cc51797c28a4301a6144eff07b751216a4"
+ "sha256:286ca070d72e250861dea7a21ab44f541cb2341e8268c70264cf8642dbd9225f"
],
"index": "pypi",
- "version": "==1.1.1"
+ "version": "==1.2"
},
"urllib3": {
"hashes": [
- "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
- "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
+ "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
+ "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
],
- "version": "==1.22"
+ "markers": "python_version != '3.0.*' and python_version < '4' and python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.6' and python_version != '3.1.*'",
+ "version": "==1.23"
},
"websockets": {
"hashes": [
@@ -537,11 +525,11 @@
},
"flake8-import-order": {
"hashes": [
- "sha256:40d2a39ed91e080f3285f4c16256b252d7c31070e7f11b7854415bb9f924ea81",
- "sha256:68d430781a9ef15c85a0121500cf8462f1a4bc7672acb2a32bfdbcab044ae0b7"
+ "sha256:9be5ca10d791d458eaa833dd6890ab2db37be80384707b0f76286ddd13c16cbf",
+ "sha256:feca2fd0a17611b33b7fa84449939196c2c82764e262486d5c3e143ed77d387b"
],
"index": "pypi",
- "version": "==0.17.1"
+ "version": "==0.18"
},
"flake8-string-format": {
"hashes": [
@@ -568,10 +556,10 @@
},
"idna": {
"hashes": [
- "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f",
- "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4"
+ "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
+ "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
- "version": "==2.6"
+ "version": "==2.7"
},
"mccabe": {
"hashes": [
@@ -604,49 +592,41 @@
"pyparsing": {
"hashes": [
"sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
- "sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07",
- "sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18",
- "sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e",
- "sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5",
- "sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58",
"sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
],
"version": "==2.2.0"
},
"pyyaml": {
"hashes": [
- "sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8",
- "sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736",
- "sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f",
- "sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608",
- "sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8",
- "sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab",
- "sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7",
- "sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3",
- "sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1",
- "sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6",
- "sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8",
- "sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
- "sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
- "sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
+ "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
+ "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
+ "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
+ "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
+ "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
+ "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
+ "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
+ "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
+ "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
+ "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
+ "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
],
"index": "pypi",
- "version": "==3.12"
+ "version": "==3.13"
},
"requests": {
"hashes": [
- "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
- "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
+ "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
+ "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
],
- "version": "==2.18.4"
+ "version": "==2.19.1"
},
"safety": {
"hashes": [
- "sha256:0bd2a26b872668767c6db8efecfc8869b547463bedff5e7cd7b52f037aa6f200",
- "sha256:fc3fc55656f1c909d65311b49a38211c42c937f57a05393289fb3f17cadfa4a1"
+ "sha256:32d41b8bbd736db749aa2162de6c0bb11c2113c7bc0357476491f96cd5d58299",
+ "sha256:34227360409ffb1bc2657e5b6ff3472a32d72b917617cd3d2914ddf078c263b9"
],
"index": "pypi",
- "version": "==1.8.1"
+ "version": "==1.8.2"
},
"six": {
"hashes": [
@@ -657,10 +637,11 @@
},
"urllib3": {
"hashes": [
- "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
- "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
+ "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
+ "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
],
- "version": "==1.22"
+ "markers": "python_version != '3.0.*' and python_version < '4' and python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.6' and python_version != '3.1.*'",
+ "version": "==1.23"
}
}
}
diff --git a/bot/__main__.py b/bot/__main__.py
index e4fe78c40..ceab87f72 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -24,7 +24,8 @@ bot = Bot(
"aliases": ["help"]
},
formatter=Formatter(),
- case_insensitive=True
+ case_insensitive=True,
+ max_messages=10_000
)
# Global aiohttp session for all cogs
@@ -47,10 +48,12 @@ else:
# Internal/debug
bot.load_extension("bot.cogs.logging")
+bot.load_extension("bot.cogs.modlog")
bot.load_extension("bot.cogs.security")
bot.load_extension("bot.cogs.events")
# Commands, etc
+bot.load_extension("bot.cogs.bigbrother")
bot.load_extension("bot.cogs.bot")
bot.load_extension("bot.cogs.cogs")
diff --git a/bot/cogs/bigbrother.py b/bot/cogs/bigbrother.py
new file mode 100644
index 000000000..4d0996122
--- /dev/null
+++ b/bot/cogs/bigbrother.py
@@ -0,0 +1,182 @@
+import logging
+from typing import List, Union
+
+from discord import Color, Embed, Guild, Member, Message, TextChannel, User
+from discord.ext.commands import Bot, Context, command
+
+from bot.constants import Channels, Emojis, Guild as GuildConfig, Keys, Roles, URLs
+from bot.decorators import with_role
+from bot.pagination import LinePaginator
+
+
+log = logging.getLogger(__name__)
+
+
+class BigBrother:
+ """User monitoring to assist with moderation."""
+
+ HEADERS = {'X-API-Key': Keys.site_api}
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+ self.watched_users = {}
+
+ def update_cache(self, api_response: List[dict]):
+ """
+ Updates the internal cache of watched users from the given `api_response`.
+ This function will only add (or update) existing keys, it will not delete
+ keys that were not present in the API response.
+ A user is only added if the bot can find a channel
+ with the given `channel_id` in its channel cache.
+ """
+
+ for entry in api_response:
+ user_id = int(entry['user_id'])
+ channel_id = int(entry['channel_id'])
+ channel = self.bot.get_channel(channel_id)
+
+ if channel is not None:
+ self.watched_users[user_id] = channel
+ else:
+ log.error(
+ f"Site specified to relay messages by `{user_id}` in `{channel_id}`, "
+ "but the given channel could not be found. Ignoring."
+ )
+
+ async def on_ready(self):
+ async with self.bot.http_session.get(URLs.site_bigbrother_api, headers=self.HEADERS) as response:
+ data = await response.json()
+ self.update_cache(data)
+
+ async def on_member_ban(self, guild: Guild, user: Union[User, Member]):
+ if guild.id == GuildConfig.id and user.id in self.watched_users:
+ url = f"{URLs.site_bigbrother_api}?user_id={user.id}"
+ channel = self.watched_users[user.id]
+
+ async with self.bot.http_session.delete(url, headers=self.HEADERS) as response:
+ del self.watched_users[user.id]
+ if response.status == 204:
+ await channel.send(
+ f"{Emojis.lemoneye2}:hammer: {user} got banned, so "
+ f"`BigBrother` will no longer relay their messages to {channel}"
+ )
+
+ else:
+ data = await response.json()
+ reason = data.get('error_message', "no message provided")
+ await channel.send(
+ f"{Emojis.lemoneye2}:x: {user} got banned, but trying to remove them from"
+ f"BigBrother's user dictionary on the API returned an error: {reason}"
+ )
+
+ async def on_message(self, msg: Message):
+ if msg.author.id in self.watched_users:
+ channel = self.watched_users[msg.author.id]
+ relay_content = (f"{Emojis.lemoneye2} {msg.author} sent the following "
+ f"in {msg.channel.mention}: {msg.clean_content}")
+ if msg.attachments:
+ relay_content += f" (with {len(msg.attachments)} attachment(s))"
+
+ await channel.send(relay_content)
+
+ @command(name='bigbrother.watched()', aliases=('bigbrother.watched',))
+ @with_role(Roles.owner, Roles.admin, Roles.moderator)
+ async def watched_command(self, ctx: Context, from_cache: bool = True):
+ """
+ Shows all users that are currently monitored and in which channel.
+ By default, the users are returned from the cache.
+ If this is not desired, `from_cache` can be given as a falsy value, e.g. e.g. 'no'.
+ """
+
+ if from_cache:
+ lines = tuple(
+ f"• <@{user_id}> in <#{self.watched_users[user_id].id}>"
+ for user_id in self.watched_users
+ )
+ await LinePaginator.paginate(
+ lines or ("There's nothing here yet.",),
+ ctx,
+ Embed(title="Watched users (cached)", color=Color.blue()),
+ empty=False
+ )
+
+ else:
+ async with self.bot.http_session.get(URLs.site_bigbrother_api, headers=self.HEADERS) as response:
+ if response.status == 200:
+ data = await response.json()
+ self.update_cache(data)
+ lines = tuple(f"• <@{entry['user_id']}> in <#{entry['channel_id']}>" for entry in data)
+
+ await LinePaginator.paginate(
+ lines or ("There's nothing here yet.",),
+ ctx,
+ Embed(title="Watched users", color=Color.blue()),
+ empty=False
+ )
+
+ else:
+ await ctx.send(f":x: got non-200 response from the API")
+
+ @command(name='bigbrother.watch()', aliases=('bigbrother.watch',))
+ @with_role(Roles.owner, Roles.admin, Roles.moderator)
+ async def watch_command(self, ctx: Context, user: User, channel: TextChannel = None):
+ """
+ Relay messages sent by the given `user` in the given `channel`.
+ If `channel` is not specified, logs to the mod log channel.
+ """
+
+ if channel is not None:
+ channel_id = channel.id
+ else:
+ channel_id = Channels.big_brother_logs
+
+ post_data = {
+ 'user_id': str(user.id),
+ 'channel_id': str(channel_id)
+ }
+
+ async with self.bot.http_session.post(
+ URLs.site_bigbrother_api,
+ headers=self.HEADERS,
+ json=post_data
+ ) as response:
+ if response.status == 204:
+ await ctx.send(f":ok_hand: will now relay messages sent by {user} in <#{channel_id}>")
+
+ channel = self.bot.get_channel(channel_id)
+ if channel is None:
+ log.error(
+ f"could not update internal cache, failed to find a channel with ID {channel_id}"
+ )
+ else:
+ self.watched_users[user.id] = channel
+
+ else:
+ data = await response.json()
+ reason = data.get('error_message', "no message provided")
+ await ctx.send(f":x: the API returned an error: {reason}")
+
+ @command(name='bigbrother.unwatch()', aliases=('bigbrother.unwatch',))
+ @with_role(Roles.owner, Roles.admin, Roles.moderator)
+ async def unwatch_command(self, ctx: Context, user: User):
+ """Stop relaying messages by the given `user`."""
+
+ url = f"{URLs.site_bigbrother_api}?user_id={user.id}"
+ async with self.bot.http_session.delete(url, headers=self.HEADERS) as response:
+ if response.status == 204:
+ await ctx.send(f":ok_hand: will no longer relay messages sent by {user}")
+
+ if user.id in self.watched_users:
+ del self.watched_users[user.id]
+ else:
+ log.warning(f"user {user.id} was unwatched but was not found in the cache")
+
+ else:
+ data = await response.json()
+ reason = data.get('error_message', "no message provided")
+ await ctx.send(f":x: the API returned an error: {reason}")
+
+
+def setup(bot: Bot):
+ bot.add_cog(BigBrother(bot))
+ log.info("Cog loaded: BigBrother")
diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py
index af9abd7cb..e79fc7ada 100644
--- a/bot/cogs/bot.py
+++ b/bot/cogs/bot.py
@@ -25,10 +25,12 @@ class Bot:
# Stores allowed channels plus epoch time since last call.
self.channel_cooldowns = {
- Channels.help0: 0,
- Channels.help1: 0,
- Channels.help2: 0,
- Channels.help3: 0,
+ Channels.help_0: 0,
+ Channels.help_1: 0,
+ Channels.help_2: 0,
+ Channels.help_3: 0,
+ Channels.help_4: 0,
+ Channels.help_5: 0,
Channels.python: 0,
}
diff --git a/bot/cogs/cogs.py b/bot/cogs/cogs.py
index 7eaf5005c..ef13aef3f 100644
--- a/bot/cogs/cogs.py
+++ b/bot/cogs/cogs.py
@@ -12,6 +12,8 @@ from bot.pagination import LinePaginator
log = logging.getLogger(__name__)
+KEEP_LOADED = ["bot.cogs.cogs", "bot.cogs.modlog"]
+
class Cogs:
"""
@@ -122,9 +124,9 @@ class Cogs:
embed.description = f"Unknown cog: {cog}"
if full_cog:
- if full_cog == "bot.cogs.cogs":
- log.warning(f"{ctx.author} requested we unload the cog management cog, that sneaky pete. We said no.")
- embed.description = "You may not unload the cog management cog!"
+ if full_cog in KEEP_LOADED:
+ log.warning(f"{ctx.author} requested we unload `{full_cog}`, that sneaky pete. We said no.")
+ embed.description = f"You may not unload `{full_cog}`!"
elif full_cog in self.bot.extensions:
try:
self.bot.unload_extension(full_cog)
diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py
index 26079e3ec..e6d108720 100644
--- a/bot/cogs/doc.py
+++ b/bot/cogs/doc.py
@@ -17,6 +17,7 @@ from sphinx.ext import intersphinx
from bot.constants import ERROR_REPLIES, Keys, Roles, URLs
from bot.converters import ValidPythonIdentifier, ValidURL
from bot.decorators import with_role
+from bot.pagination import LinePaginator
log = logging.getLogger(__name__)
@@ -371,15 +372,13 @@ class Doc:
"""
if symbol is None:
- all_inventories = "\n".join(
- f"• [`{name}`]({url})" for name, url in self.base_urls.items()
- )
inventory_embed = discord.Embed(
- title="All inventories",
- description=all_inventories or "*Seems like there's nothing here yet.*",
+ title=f"All inventories (`{len(self.base_urls)}` total)",
colour=discord.Colour.blue()
)
- await ctx.send(embed=inventory_embed)
+
+ lines = sorted(f"• [`{name}`]({url})" for name, url in self.base_urls.items())
+ await LinePaginator.paginate(lines, ctx, inventory_embed, max_size=400, empty=False)
else:
# Fetching documentation for a symbol (at least for the first time, since
diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py
new file mode 100644
index 000000000..87cea2b5a
--- /dev/null
+++ b/bot/cogs/modlog.py
@@ -0,0 +1,658 @@
+import asyncio
+import datetime
+import logging
+from typing import List, Optional, Union
+
+from dateutil.relativedelta import relativedelta
+from deepdiff import DeepDiff
+from discord import (
+ CategoryChannel, Colour, Embed, File, Guild,
+ Member, Message, NotFound, RawBulkMessageDeleteEvent,
+ RawMessageDeleteEvent, RawMessageUpdateEvent, Role,
+ TextChannel, User, VoiceChannel)
+from discord.abc import GuildChannel
+from discord.ext.commands import Bot
+
+from bot.constants import Channels, Emojis, Icons
+from bot.constants import Guild as GuildConstant
+from bot.utils.time import humanize
+
+
+log = logging.getLogger(__name__)
+
+BULLET_POINT = "\u2022"
+COLOUR_RED = Colour(0xcd6d6d)
+COLOUR_GREEN = Colour(0x68c290)
+GUILD_CHANNEL = Union[CategoryChannel, TextChannel, VoiceChannel]
+
+CHANNEL_CHANGES_UNSUPPORTED = ("permissions",)
+CHANNEL_CHANGES_SUPPRESSED = ("_overwrites", "position")
+MEMBER_CHANGES_SUPPRESSED = ("activity", "status")
+ROLE_CHANGES_UNSUPPORTED = ("colour", "permissions")
+
+
+class ModLog:
+ """
+ Logging for server events and staff actions
+ """
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+ self._ignored_deletions = []
+
+ self._cached_deletes = []
+ self._cached_edits = []
+
+ def ignore_message_deletion(self, *message_ids: int):
+ for message_id in message_ids:
+ if message_id not in self._ignored_deletions:
+ self._ignored_deletions.append(message_id)
+
+ async def send_log_message(
+ self, icon_url: Optional[str], colour: Colour, title: Optional[str], text: str, thumbnail: str = None,
+ channel_id: int = Channels.modlog, ping_everyone: bool = False, files: List[File] = None
+ ):
+ embed = Embed(description=text)
+
+ if title and icon_url:
+ embed.set_author(name=title, icon_url=icon_url)
+
+ embed.colour = colour
+ embed.timestamp = datetime.datetime.utcnow()
+
+ if thumbnail is not None:
+ embed.set_thumbnail(url=thumbnail)
+
+ content = None
+
+ if ping_everyone:
+ content = "@everyone"
+
+ await self.bot.get_channel(channel_id).send(content=content, embed=embed, files=files)
+
+ async def on_guild_channel_create(self, channel: GUILD_CHANNEL):
+ if channel.guild.id != GuildConstant.id:
+ return
+
+ if isinstance(channel, CategoryChannel):
+ title = "Category created"
+ message = f"{channel.name} (`{channel.id}`)"
+ elif isinstance(channel, VoiceChannel):
+ title = "Voice channel created"
+
+ if channel.category:
+ message = f"{channel.category}/{channel.name} (`{channel.id}`)"
+ else:
+ message = f"{channel.name} (`{channel.id}`)"
+ else:
+ title = "Text channel created"
+
+ if channel.category:
+ message = f"{channel.category}/{channel.name} (`{channel.id}`)"
+ else:
+ message = f"{channel.name} (`{channel.id}`)"
+
+ await self.send_log_message(Icons.hash_green, COLOUR_GREEN, title, message)
+
+ async def on_guild_channel_delete(self, channel: GUILD_CHANNEL):
+ if channel.guild.id != GuildConstant.id:
+ return
+
+ if isinstance(channel, CategoryChannel):
+ title = "Category deleted"
+ elif isinstance(channel, VoiceChannel):
+ title = "Voice channel deleted"
+ else:
+ title = "Text channel deleted"
+
+ if channel.category and not isinstance(channel, CategoryChannel):
+ message = f"{channel.category}/{channel.name} (`{channel.id}`)"
+ else:
+ message = f"{channel.name} (`{channel.id}`)"
+
+ await self.send_log_message(
+ Icons.hash_red, COLOUR_RED,
+ title, message
+ )
+
+ async def on_guild_channel_update(self, before: GUILD_CHANNEL, after: GuildChannel):
+ if before.guild.id != GuildConstant.id:
+ return
+
+ diff = DeepDiff(before, after)
+ changes = []
+ done = []
+
+ diff_values = diff.get("values_changed", {})
+ diff_values.update(diff.get("type_changes", {}))
+
+ for key, value in diff_values.items():
+ if not key: # Not sure why, but it happens
+ continue
+
+ key = key[5:] # Remove "root." prefix
+
+ if "[" in key:
+ key = key.split("[", 1)[0]
+
+ if "." in key:
+ key = key.split(".", 1)[0]
+
+ if key in done or key in CHANNEL_CHANGES_SUPPRESSED:
+ continue
+
+ if key in CHANNEL_CHANGES_UNSUPPORTED:
+ changes.append(f"**{key.title()}** updated")
+ else:
+ new = value["new_value"]
+ old = value["old_value"]
+
+ changes.append(f"**{key.title()}:** `{old}` **->** `{new}`")
+
+ done.append(key)
+
+ if not changes:
+ return
+
+ message = ""
+
+ for item in sorted(changes):
+ message += f"{BULLET_POINT} {item}\n"
+
+ if after.category:
+ message = f"**{after.category}/#{after.name} (`{after.id}`)**\n{message}"
+ else:
+ message = f"**#{after.name}** (`{after.id}`)\n{message}"
+
+ await self.send_log_message(
+ Icons.hash_blurple, Colour.blurple(),
+ "Channel updated", message
+ )
+
+ async def on_guild_role_create(self, role: Role):
+ if role.guild.id != GuildConstant.id:
+ return
+
+ await self.send_log_message(
+ Icons.crown_green, COLOUR_GREEN,
+ "Role created", f"`{role.id}`"
+ )
+
+ async def on_guild_role_delete(self, role: Role):
+ if role.guild.id != GuildConstant.id:
+ return
+
+ await self.send_log_message(
+ Icons.crown_red, COLOUR_RED,
+ "Role removed", f"{role.name} (`{role.id}`)"
+ )
+
+ async def on_guild_role_update(self, before: Role, after: Role):
+ if before.guild.id != GuildConstant.id:
+ return
+
+ diff = DeepDiff(before, after)
+ changes = []
+ done = []
+
+ diff_values = diff.get("values_changed", {})
+ diff_values.update(diff.get("type_changes", {}))
+
+ for key, value in diff_values.items():
+ if not key: # Not sure why, but it happens
+ continue
+
+ key = key[5:] # Remove "root." prefix
+
+ if "[" in key:
+ key = key.split("[", 1)[0]
+
+ if "." in key:
+ key = key.split(".", 1)[0]
+
+ if key in done or key == "color":
+ continue
+
+ if key in ROLE_CHANGES_UNSUPPORTED:
+ changes.append(f"**{key.title()}** updated")
+ else:
+ new = value["new_value"]
+ old = value["old_value"]
+
+ changes.append(f"**{key.title()}:** `{old}` **->** `{new}`")
+
+ done.append(key)
+
+ if not changes:
+ return
+
+ message = ""
+
+ for item in sorted(changes):
+ message += f"{BULLET_POINT} {item}\n"
+
+ message = f"**{after.name}** (`{after.id}`)\n{message}"
+
+ await self.send_log_message(
+ Icons.crown_blurple, Colour.blurple(),
+ "Role updated", message
+ )
+
+ async def on_guild_update(self, before: Guild, after: Guild):
+ if before.id != GuildConstant.id:
+ return
+
+ diff = DeepDiff(before, after)
+ changes = []
+ done = []
+
+ diff_values = diff.get("values_changed", {})
+ diff_values.update(diff.get("type_changes", {}))
+
+ for key, value in diff_values.items():
+ if not key: # Not sure why, but it happens
+ continue
+
+ key = key[5:] # Remove "root." prefix
+
+ if "[" in key:
+ key = key.split("[", 1)[0]
+
+ if "." in key:
+ key = key.split(".", 1)[0]
+
+ if key in done:
+ continue
+
+ new = value["new_value"]
+ old = value["old_value"]
+
+ changes.append(f"**{key.title()}:** `{old}` **->** `{new}`")
+
+ done.append(key)
+
+ if not changes:
+ return
+
+ message = ""
+
+ for item in sorted(changes):
+ message += f"{BULLET_POINT} {item}\n"
+
+ message = f"**{after.name}** (`{after.id}`)\n{message}"
+
+ await self.send_log_message(
+ Icons.guild_update, Colour.blurple(),
+ "Guild updated", message,
+ thumbnail=after.icon_url_as(format="png")
+ )
+
+ async def on_member_ban(self, guild: Guild, member: Union[Member, User]):
+ if guild.id != GuildConstant.id:
+ return
+
+ await self.send_log_message(
+ Icons.user_ban, COLOUR_RED,
+ "User banned", f"{member.name}#{member.discriminator} (`{member.id}`)",
+ thumbnail=member.avatar_url_as(static_format="png")
+ )
+
+ async def on_member_join(self, member: Member):
+ if member.guild.id != GuildConstant.id:
+ return
+
+ message = f"{member.name}#{member.discriminator} (`{member.id}`)"
+
+ now = datetime.datetime.utcnow()
+ difference = abs(relativedelta(now, member.created_at))
+
+ message += "\n\n**Account age:** " + humanize(difference)
+
+ if difference.days < 1 and difference.months < 1 and difference.years < 1: # New user account!
+ message = f"{Emojis.new} {message}"
+
+ await self.send_log_message(
+ Icons.sign_in, COLOUR_GREEN,
+ "User joined", message,
+ thumbnail=member.avatar_url_as(static_format="png")
+ )
+
+ async def on_member_remove(self, member: Member):
+ if member.guild.id != GuildConstant.id:
+ return
+
+ await self.send_log_message(
+ Icons.sign_out, COLOUR_RED,
+ "User left", f"{member.name}#{member.discriminator} (`{member.id}`)",
+ thumbnail=member.avatar_url_as(static_format="png")
+ )
+
+ async def on_member_unban(self, guild: Guild, member: User):
+ if guild.id != GuildConstant.id:
+ return
+
+ await self.send_log_message(
+ Icons.user_unban, Colour.blurple(),
+ "User unbanned", f"{member.name}#{member.discriminator} (`{member.id}`)",
+ thumbnail=member.avatar_url_as(static_format="png")
+ )
+
+ async def on_member_update(self, before: Member, after: Member):
+ if before.guild.id != GuildConstant.id:
+ return
+
+ diff = DeepDiff(before, after)
+ changes = []
+ done = []
+
+ diff_values = {}
+
+ diff_values.update(diff.get("values_changed", {}))
+ diff_values.update(diff.get("type_changes", {}))
+ diff_values.update(diff.get("iterable_item_removed", {}))
+ diff_values.update(diff.get("iterable_item_added", {}))
+
+ diff_user = DeepDiff(before._user, after._user)
+
+ diff_values.update(diff_user.get("values_changed", {}))
+ diff_values.update(diff_user.get("type_changes", {}))
+ diff_values.update(diff_user.get("iterable_item_removed", {}))
+ diff_values.update(diff_user.get("iterable_item_added", {}))
+
+ for key, value in diff_values.items():
+ if not key: # Not sure why, but it happens
+ continue
+
+ key = key[5:] # Remove "root." prefix
+
+ if "[" in key:
+ key = key.split("[", 1)[0]
+
+ if "." in key:
+ key = key.split(".", 1)[0]
+
+ if key in done or key in MEMBER_CHANGES_SUPPRESSED:
+ continue
+
+ if key == "roles":
+ new_roles = after.roles
+ old_roles = before.roles
+
+ for role in old_roles:
+ if role not in new_roles:
+ changes.append(f"**Role removed:** {role.name} (`{role.id}`)")
+
+ for role in new_roles:
+ if role not in old_roles:
+ changes.append(f"**Role added:** {role.name} (`{role.id}`)")
+
+ else:
+ new = value["new_value"]
+ old = value["old_value"]
+
+ changes.append(f"**{key.title()}:** `{old}` **->** `{new}`")
+
+ done.append(key)
+
+ if before.name != after.name:
+ changes.append(
+ f"**Username:** `{before.name}` **->** `{after.name}`"
+ )
+
+ if before.discriminator != after.discriminator:
+ changes.append(
+ f"**Discriminator:** `{before.discriminator}` **->** `{after.discriminator}`"
+ )
+
+ if not changes:
+ return
+
+ message = ""
+
+ for item in sorted(changes):
+ message += f"{BULLET_POINT} {item}\n"
+
+ message = f"**{after.name}#{after.discriminator}** (`{after.id}`)\n{message}"
+
+ await self.send_log_message(
+ Icons.user_update, Colour.blurple(),
+ "Member updated", message,
+ thumbnail=after.avatar_url_as(static_format="png")
+ )
+
+ async def on_raw_bulk_message_delete(self, event: RawBulkMessageDeleteEvent):
+ if event.guild_id != GuildConstant.id or event.channel_id in GuildConstant.ignored:
+ return
+
+ # Could upload the log to the site - maybe we should store all the messages somewhere?
+ # Currently if messages aren't in the cache, we ain't gonna have 'em.
+
+ ignored_messages = 0
+
+ for message_id in event.message_ids:
+ if message_id in self._ignored_deletions:
+ self._ignored_deletions.remove(message_id)
+ ignored_messages += 1
+
+ if ignored_messages >= len(event.message_ids):
+ return
+
+ channel = self.bot.get_channel(event.channel_id)
+
+ if channel.category:
+ message = f"{len(event.message_ids)} deleted in {channel.category}/#{channel.name} (`{channel.id}`)"
+ else:
+ message = f"{len(event.message_ids)} deleted in #{channel.name} (`{channel.id}`)"
+
+ await self.send_log_message(
+ Icons.message_bulk_delete, Colour.orange(),
+ "Bulk message delete",
+ message, channel_id=Channels.devalerts,
+ ping_everyone=True
+ )
+
+ async def on_message_delete(self, message: Message):
+ channel = message.channel
+ author = message.author
+
+ if message.guild.id != GuildConstant.id or channel.id in GuildConstant.ignored:
+ return
+
+ self._cached_deletes.append(message.id)
+
+ if message.id in self._ignored_deletions:
+ self._ignored_deletions.remove(message.id)
+ return
+
+ if author.bot:
+ return
+
+ if channel.category:
+ response = (
+ f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
+ f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{message.id}`\n"
+ "\n"
+ f"{message.clean_content}"
+ )
+ else:
+ response = (
+ f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
+ f"**Channel:** #{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{message.id}`\n"
+ "\n"
+ f"{message.clean_content}"
+ )
+
+ if message.attachments:
+ # Prepend the message metadata with the number of attachments
+ response = f"**Attachments:** {len(message.attachments)}\n" + response
+
+ await self.send_log_message(
+ Icons.message_delete, COLOUR_RED,
+ "Message deleted",
+ response,
+ channel_id=Channels.message_log
+ )
+
+ async def on_raw_message_delete(self, event: RawMessageDeleteEvent):
+ if event.guild_id != GuildConstant.id or event.channel_id in GuildConstant.ignored:
+ return
+
+ await asyncio.sleep(1) # Wait here in case the normal event was fired
+
+ if event.message_id in self._cached_deletes:
+ # It was in the cache and the normal event was fired, so we can just ignore it
+ self._cached_deletes.remove(event.message_id)
+ return
+
+ if event.message_id in self._ignored_deletions:
+ self._ignored_deletions.remove(event.message_id)
+ return
+
+ channel = self.bot.get_channel(event.channel_id)
+
+ if channel.category:
+ response = (
+ f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{event.message_id}`\n"
+ "\n"
+ "This message was not cached, so the message content cannot be displayed."
+ )
+ else:
+ response = (
+ f"**Channel:** #{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{event.message_id}`\n"
+ "\n"
+ "This message was not cached, so the message content cannot be displayed."
+ )
+
+ await self.send_log_message(
+ Icons.message_delete, COLOUR_RED,
+ "Message deleted",
+ response,
+ channel_id=Channels.message_log
+ )
+
+ async def on_message_edit(self, before: Message, after: Message):
+ if before.guild.id != GuildConstant.id or before.channel.id in GuildConstant.ignored or before.author.bot:
+ return
+
+ self._cached_edits.append(before.id)
+
+ if before.content == after.content:
+ return
+
+ author = before.author
+ channel = before.channel
+
+ if channel.category:
+ before_response = (
+ f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
+ f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{before.id}`\n"
+ "\n"
+ f"{before.clean_content}"
+ )
+
+ after_response = (
+ f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
+ f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{before.id}`\n"
+ "\n"
+ f"{after.clean_content}"
+ )
+ else:
+ before_response = (
+ f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
+ f"**Channel:** #{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{before.id}`\n"
+ "\n"
+ f"{before.clean_content}"
+ )
+
+ after_response = (
+ f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
+ f"**Channel:** #{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{before.id}`\n"
+ "\n"
+ f"{after.clean_content}"
+ )
+
+ await self.send_log_message(
+ Icons.message_edit, Colour.blurple(), "Message edited (Before)",
+ before_response, channel_id=Channels.message_log
+ )
+
+ await self.send_log_message(
+ Icons.message_edit, Colour.blurple(), "Message edited (After)",
+ after_response, channel_id=Channels.message_log
+ )
+
+ async def on_raw_message_edit(self, event: RawMessageUpdateEvent):
+ try:
+ channel = self.bot.get_channel(int(event.data["channel_id"]))
+ message = await channel.get_message(event.message_id)
+ except NotFound: # Was deleted before we got the event
+ return
+
+ if message.guild.id != GuildConstant.id or message.channel.id in GuildConstant.ignored or message.author.bot:
+ return
+
+ await asyncio.sleep(1) # Wait here in case the normal event was fired
+
+ if event.message_id in self._cached_edits:
+ # It was in the cache and the normal event was fired, so we can just ignore it
+ self._cached_edits.remove(event.message_id)
+ return
+
+ author = message.author
+ channel = message.channel
+
+ if channel.category:
+ before_response = (
+ f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
+ f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{message.id}`\n"
+ "\n"
+ "This message was not cached, so the message content cannot be displayed."
+ )
+
+ after_response = (
+ f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
+ f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{message.id}`\n"
+ "\n"
+ f"{message.clean_content}"
+ )
+ else:
+ before_response = (
+ f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
+ f"**Channel:** #{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{message.id}`\n"
+ "\n"
+ "This message was not cached, so the message content cannot be displayed."
+ )
+
+ after_response = (
+ f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n"
+ f"**Channel:** #{channel.name} (`{channel.id}`)\n"
+ f"**Message ID:** `{message.id}`\n"
+ "\n"
+ f"{message.clean_content}"
+ )
+
+ await self.send_log_message(
+ Icons.message_edit, Colour.blurple(), "Message edited (Before)",
+ before_response, channel_id=Channels.message_log
+ )
+
+ await self.send_log_message(
+ Icons.message_edit, Colour.blurple(), "Message edited (After)",
+ after_response, channel_id=Channels.message_log
+ )
+
+
+def setup(bot):
+ bot.add_cog(ModLog(bot))
+ log.info("Cog loaded: ModLog")
diff --git a/bot/cogs/snekbox.py b/bot/cogs/snekbox.py
index 6bc333ff6..69a2ed59e 100644
--- a/bot/cogs/snekbox.py
+++ b/bot/cogs/snekbox.py
@@ -1,11 +1,16 @@
import datetime
import logging
+import random
import re
-from discord.ext.commands import Bot, Context, command
+from discord import Colour, Embed
+from discord.ext.commands import (
+ Bot, CommandError, Context, MissingPermissions,
+ NoPrivateMessage, check, command, guild_only
+)
from bot.cogs.rmq import RMQ
-from bot.constants import URLs
+from bot.constants import Channels, ERROR_REPLIES, NEGATIVE_REPLIES, Roles, URLs
log = logging.getLogger(__name__)
@@ -26,6 +31,21 @@ except Exception as e:
"""
ESCAPE_REGEX = re.compile("[`\u202E\u200B]{3,}")
+BYPASS_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers)
+WHITELISTED_CHANNELS = (Channels.bot,)
+WHITELISTED_CHANNELS_STRING = ', '.join(f"<#{channel_id}>" for channel_id in WHITELISTED_CHANNELS)
+
+
+async def channel_is_whitelisted_or_author_can_bypass(ctx: Context):
+ """
+ Checks that the author is either helper or above
+ or the channel is a whitelisted channel.
+ """
+
+ if ctx.channel.id not in WHITELISTED_CHANNELS and ctx.author.top_role.id not in BYPASS_ROLES:
+ raise MissingPermissions("You are not allowed to do that here.")
+
+ return True
class Snekbox:
@@ -44,6 +64,8 @@ class Snekbox:
return self.bot.get_cog("RMQ")
@command(name="snekbox.eval()", aliases=["snekbox.eval", "eval()", "eval"])
+ @guild_only()
+ @check(channel_is_whitelisted_or_author_can_bypass)
async def do_eval(self, ctx: Context, code: str):
"""
Run some code. get the result back. We've done our best to make this safe, but do let us know if you
@@ -81,6 +103,9 @@ class Snekbox:
if "<@" in output:
output = output.replace("<@", "<@\u200B") # Zero-width space
+ if "<!@" in output:
+ output = output.replace("<!@", "<!@\u200B") # Zero-width space
+
if ESCAPE_REGEX.findall(output):
output = "Code block escape attempt detected; will not output result"
else:
@@ -134,6 +159,27 @@ class Snekbox:
del self.jobs[ctx.author.id]
raise
+ @do_eval.error
+ async def eval_command_error(self, ctx: Context, error: CommandError):
+ embed = Embed(colour=Colour.red())
+
+ if isinstance(error, NoPrivateMessage):
+ embed.title = random.choice(NEGATIVE_REPLIES)
+ embed.description = "You're not allowed to use this command in private messages."
+ await ctx.send(embed=embed)
+
+ elif isinstance(error, MissingPermissions):
+ embed.title = random.choice(NEGATIVE_REPLIES)
+ embed.description = f"Sorry, but you may only use this command within {WHITELISTED_CHANNELS_STRING}."
+ await ctx.send(embed=embed)
+
+ else:
+ original_error = getattr(error, 'original', "no original error")
+ log.error(f"Unhandled error in snekbox eval: {error} ({original_error})")
+ embed.title = random.choice(ERROR_REPLIES)
+ embed.description = "Some unhandled error occurred. Sorry for that!"
+ await ctx.send(embed=embed)
+
def setup(bot):
bot.add_cog(Snekbox(bot))
diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py
index 198fe861d..621610903 100644
--- a/bot/cogs/verification.py
+++ b/bot/cogs/verification.py
@@ -3,6 +3,7 @@ import logging
from discord import Message, NotFound, Object
from discord.ext.commands import Bot, Context, command
+from bot.cogs.modlog import ModLog
from bot.constants import Channels, Roles
from bot.decorators import in_channel, without_role
@@ -35,6 +36,10 @@ class Verification:
def __init__(self, bot: Bot):
self.bot = bot
+ @property
+ def modlog(self) -> ModLog:
+ return self.bot.get_cog("ModLog")
+
async def on_message(self, message: Message):
if message.author.bot:
return # They're a bot, ignore
@@ -85,6 +90,7 @@ class Verification:
log.trace(f"Deleting the message posted by {ctx.author}.")
try:
+ self.modlog.ignore_message_deletion(ctx.message.id)
await ctx.message.delete()
except NotFound:
log.trace("No message found, it must have been deleted by another bot.")
diff --git a/bot/constants.py b/bot/constants.py
index 163066fc0..4a3b4f133 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -14,9 +14,10 @@ import logging
import os
from collections.abc import Mapping
from pathlib import Path
+from typing import List
import yaml
-
+from yaml.constructor import ConstructorError
log = logging.getLogger(__name__)
@@ -59,12 +60,42 @@ def _env_var_constructor(loader, node):
key: !ENV 'MY_APP_KEY'
"""
- value = loader.construct_scalar(node)
- return os.getenv(value)
+ default = None
+
+ try:
+ # Try to construct a list from this YAML node
+ value = loader.construct_sequence(node)
+
+ if len(value) >= 2:
+ # If we have at least two values, then we have both a key and a default value
+ default = value[1]
+ key = value[0]
+ else:
+ # Otherwise, we just have a key
+ key = value[0]
+ except ConstructorError:
+ # This YAML node is a plain value rather than a list, so we just have a key
+ value = loader.construct_scalar(node)
+
+ key = str(value)
+
+ return os.getenv(key, default)
+
+
+def _join_var_constructor(loader, node):
+ """
+ Implements a custom YAML tag for concatenating other tags in
+ the document to strings. This allows for a much more DRY configuration
+ file.
+ """
+
+ fields = loader.construct_sequence(node)
+ return "".join(str(x) for x in fields)
-yaml.SafeLoader.add_constructor("!REQUIRED_ENV", _required_env_var_constructor)
yaml.SafeLoader.add_constructor("!ENV", _env_var_constructor)
+yaml.SafeLoader.add_constructor("!JOIN", _join_var_constructor)
+yaml.SafeLoader.add_constructor("!REQUIRED_ENV", _required_env_var_constructor)
with open("config-default.yml") as f:
@@ -175,23 +206,60 @@ class Emojis(metaclass=YAMLGetter):
red_chevron: str
white_chevron: str
+ new: str
+ pencil: str
+
+
+class Icons(metaclass=YAMLGetter):
+ section = "bot"
+ subsection = "icons"
+
+ crown_blurple: str
+ crown_green: str
+ crown_red: str
+
+ guild_update: str
+
+ hash_blurple: str
+ hash_green: str
+ hash_red: str
+
+ message_bulk_delete: str
+ message_delete: str
+ message_edit: str
+
+ sign_in: str
+ sign_out: str
+
+ user_ban: str
+ user_unban: str
+ user_update: str
+
class Channels(metaclass=YAMLGetter):
section = "guild"
subsection = "channels"
+ admins: int
announcements: int
+ big_brother_logs: int
bot: int
checkpoint_test: int
+ devalerts: int
devlog: int
devtest: int
- help0: int
- help1: int
- help2: int
- help3: int
- help4: int
+ help_0: int
+ help_1: int
+ help_2: int
+ help_3: int
+ help_4: int
+ help_5: int
helpers: int
+ message_log: int
modlog: int
+ off_topic_1: int
+ off_topic_2: int
+ off_topic_3: int
python: int
verification: int
@@ -216,6 +284,7 @@ class Guild(metaclass=YAMLGetter):
section = "guild"
id: int
+ ignored: List[int]
class Keys(metaclass=YAMLGetter):
@@ -258,6 +327,7 @@ class URLs(metaclass=YAMLGetter):
site_idioms_api: str
site_names_api: str
site_quiz_api: str
+ site_schema: str
site_settings_api: str
site_special_api: str
site_tags_api: str
diff --git a/bot/utils/time.py b/bot/utils/time.py
new file mode 100644
index 000000000..b3f55932c
--- /dev/null
+++ b/bot/utils/time.py
@@ -0,0 +1,59 @@
+from dateutil.relativedelta import relativedelta
+
+
+def _plural_timestring(value: int, unit: str) -> str:
+ """
+ Takes a value and a unit type,
+ such as 24 and "hours".
+
+ Returns a string that takes
+ the correct plural into account.
+
+ >>> _plural_timestring(1, "seconds")
+ "1 second"
+ >>> _plural_timestring(24, "hours")
+ "24 hours"
+ """
+
+ if value == 1:
+ return f"{value} {unit[:-1]}"
+ else:
+ return f"{value} {unit}"
+
+
+def humanize(delta: relativedelta, accuracy: str = "seconds") -> str:
+ """
+ This takes a relativedelta and
+ returns a nice human readable string.
+
+ "4 days, 12 hours and 1 second"
+
+ :param delta: A dateutils.relativedelta.relativedelta object
+ :param accuracy: The smallest unit that should be included.
+ :return: A humanized string.
+ """
+
+ units = {
+ "years": delta.years,
+ "months": delta.months,
+ "days": delta.days,
+ "hours": delta.hours,
+ "minutes": delta.minutes,
+ "seconds": delta.seconds
+ }
+
+ # Add the time units that are >0, but stop at accuracy.
+ time_strings = []
+ for unit, value in units.items():
+ if value:
+ time_strings.append(_plural_timestring(value, unit))
+
+ if unit == accuracy:
+ break
+
+ # Add the 'and' between the last two units
+ if len(time_strings) > 1:
+ time_strings[-1] = f"{time_strings[-2]} and {time_strings[-1]}"
+ del time_strings[-2]
+
+ return ", ".join(time_strings)
diff --git a/config-default.yml b/config-default.yml
index b7efb0010..dc193f149 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -1,38 +1,71 @@
bot:
- help_prefix: 'bot.'
- token: !REQUIRED_ENV 'BOT_TOKEN'
+ help_prefix: "bot."
+ token: !REQUIRED_ENV "BOT_TOKEN"
cooldowns:
# Per channel, per tag.
tags: 60
emojis:
- green_chevron: '<:greenchevron:418104310329769993>'
- red_chevron: '<:redchevron:418112778184818698>'
- white_chevron: '<:whitechevron:418110396973711363>'
+ green_chevron: "<:greenchevron:418104310329769993>"
+ red_chevron: "<:redchevron:418112778184818698>"
+ white_chevron: "<:whitechevron:418110396973711363>"
+ lemoneye2: "<:lemoneye2:435193765582340098>"
+
+ pencil: "\u270F"
+ new: "\U0001F195"
+
+ icons:
+ crown_blurple: "https://cdn.discordapp.com/emojis/469964153289965568.png"
+ crown_green: "https://cdn.discordapp.com/emojis/469964154719961088.png"
+ crown_red: "https://cdn.discordapp.com/emojis/469964154879344640.png"
+
+ guild_update: "https://cdn.discordapp.com/emojis/469954765141442561.png"
+
+ hash_blurple: "https://cdn.discordapp.com/emojis/469950142942806017.png"
+ hash_green: "https://cdn.discordapp.com/emojis/469950144918585344.png"
+ hash_red: "https://cdn.discordapp.com/emojis/469950145413251072.png"
+
+ message_bulk_delete: "https://cdn.discordapp.com/emojis/469952898994929668.png"
+ message_delete: "https://cdn.discordapp.com/emojis/469952898516779008.png"
+ message_edit: "https://cdn.discordapp.com/emojis/469952898143485972.png"
+
+ sign_in: "https://cdn.discordapp.com/emojis/469952898181234698.png"
+ sign_out: "https://cdn.discordapp.com/emojis/469952898089091082.png"
+
+ user_ban: "https://cdn.discordapp.com/emojis/469952898026045441.png"
+ user_unban: "https://cdn.discordapp.com/emojis/469952898692808704.png"
+ user_update: "https://cdn.discordapp.com/emojis/469952898684551168.png"
guild:
id: 267624335836053506
channels:
- announcements: 354619224620138496
- bot: 267659945086812160
- checkpoint_test: 422077681434099723
- devlog: 409308876241108992
- devtest: 414574275865870337
- help0: 303906576991780866
- help1: 303906556754395136
- help2: 303906514266226689
- help3: 439702951246692352
- help4: 451312046647148554
- helpers: 385474242440986624
- modlog: 282638479504965634
- python: 267624335836053506
- verification: 352442727016693763
- off_topic_0: 291284109232308226
- off_topic_1: 463035241142026251
- off_topic_2: 463035268514185226
+ admins: &ADMINS 365960823622991872
+ announcements: 354619224620138496
+ big_brother_logs: 468507907357409333
+ bot: 267659945086812160
+ checkpoint_test: 422077681434099723
+ devalerts: 460181980097675264
+ devlog: 409308876241108992
+ devtest: 414574275865870337
+ help_0: 303906576991780866
+ help_1: 303906556754395136
+ help_2: 303906514266226689
+ help_3: 439702951246692352
+ help_4: 451312046647148554
+ help_5: 454941769734422538
+ helpers: 385474242440986624
+ message_log: &MESSAGE_LOG 467752170159079424
+ modlog: &MODLOG 282638479504965634
+ off_topic_0: 291284109232308226
+ off_topic_1: 463035241142026251
+ off_topic_2: 463035268514185226
+ python: 267624335836053506
+ verification: 352442727016693763
+
+ ignored: [*ADMINS, *MESSAGE_LOG, *MODLOG]
roles:
admin: 267628507062992896
@@ -44,52 +77,61 @@ guild:
moderator: 267629731250176001
owner: 267627879762755584
verified: 352427296948486144
+ helpers: 267630620367257601
muted: 0
keys:
- deploy_bot: !ENV 'DEPLOY_BOT_KEY'
- deploy_site: !ENV 'DEPLOY_SITE'
- omdb: !ENV 'OMDB_API_KEY'
- site_api: !ENV 'BOT_API_KEY'
- youtube: !ENV 'YOUTUBE_API_KEY'
+ deploy_bot: !ENV "DEPLOY_BOT_KEY"
+ deploy_site: !ENV "DEPLOY_SITE"
+ omdb: !ENV "OMDB_API_KEY"
+ site_api: !ENV "BOT_API_KEY"
+ youtube: !ENV "YOUTUBE_API_KEY"
clickup:
- key: !ENV 'CLICKUP_KEY'
- space: 757069
- team: 754996
+ key: !ENV "CLICKUP_KEY"
+ space: 757069
+ team: 754996
rabbitmq:
- host: "pdrmq"
- password: !ENV "RABBITMQ_DEFAULT_PASS"
- port: 5672
- username: !ENV "RABBITMQ_DEFAULT_USER"
+ host: "pdrmq"
+ password: !ENV ["RABBITMQ_DEFAULT_PASS", "guest"]
+ port: 5672
+ username: !ENV ["RABBITMQ_DEFAULT_USER", "guest"]
urls:
- bot_avatar: 'https://raw.githubusercontent.com/discord-python/branding/master/logos/logo_circle/logo_circle.png'
- deploy: !ENV 'DEPLOY_URL'
- gitlab_bot_repo: 'https://gitlab.com/discord-python/projects/bot'
- omdb: 'http://omdbapi.com'
- site: 'pythondiscord.com'
- site_docs_api: 'https://api.pythondiscord.com/bot/docs'
- site_facts_api: 'https://api.pythondiscord.com/bot/snake_facts'
- site_hiphopify_api: 'https://api.pythondiscord.com/bot/hiphopify'
- site_idioms_api: 'https://api.pythondiscord.com/bot/snake_idioms'
- site_names_api: 'https://api.pythondiscord.com/bot/snake_names'
- site_off_topic_names_api: 'https://api.pythondiscord.com/bot/off-topic-names'
- site_quiz_api: 'https://api.pythondiscord.com/bot/snake_quiz'
- site_settings_api: 'https://api.pythondiscord.com/bot/settings'
- site_special_api: 'https://api.pythondiscord.com/bot/special_snakes'
- site_tags_api: 'https://api.pythondiscord.com/bot/tags'
- site_user_api: 'https://api.pythondiscord.com/bot/users'
- site_user_complete_api: 'https://api.pythondiscord.com/bot/users/complete'
- site_infractions: 'https://api.pythondiscord.com/bot/infractions'
- site_infractions_user: 'https://api.pythondiscord.com/bot/infractions/user/{user_id}'
- site_infractions_type: 'https://api.pythondiscord.com/bot/infractions/type/{infraction_type}'
- site_infractions_by_id: 'https://api.pythondiscord.com/bot/infractions/id/{infraction_id}'
- site_infractions_user_type_current: 'https://api.pythondiscord.com/bot/infractions/user/{user_id}/{infraction_type}/current'
- status: !ENV 'STATUS_URL'
- paste_service: 'https://paste.pydis.com/{key}'
+ # PyDis site vars
+ site: &DOMAIN "api.pythondiscord.com"
+ site_schema: &SCHEMA "https://"
+
+ site_bigbrother_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/bigbrother"]
+ site_docs_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/docs"]
+ site_facts_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_facts"]
+ site_hiphopify_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/hiphopify"]
+ site_idioms_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_idioms"]
+ site_names_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_names"]
+ site_off_topic_names_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/off-topic-names"]
+ site_quiz_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_quiz"]
+ site_settings_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/settings"]
+ site_special_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/special_snakes"]
+ site_tags_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/tags"]
+ site_user_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/users"]
+ site_user_complete_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/users/complete"]
+ site_infractions: !JOIN [*SCHEMA, *DOMAIN, "/bot/infractions"]
+ site_infractions_user: !JOIN [*SCHEMA, *DOMAIN, "/bot/infractions/user/{user_id}"]
+ site_infractions_type: !JOIN [*SCHEMA, *DOMAIN, "/bot/infractions/type/{infraction_type}"]
+ site_infractions_by_id: !JOIN [*SCHEMA, *DOMAIN, "/bot/infractions/id/{infraction_id}"]
+ site_infractions_user_type_current: !JOIN [*SCHEMA, *DOMAIN, "/bot/infractions/user/{user_id}/{infraction_type}/current"]
+
+ # Env vars
+ deploy: !ENV "DEPLOY_URL"
+ status: !ENV "STATUS_URL"
+
+ # Misc URLs
+ bot_avatar: "https://raw.githubusercontent.com/discord-python/branding/master/logos/logo_circle/logo_circle.png"
+ gitlab_bot_repo: "https://gitlab.com/python-discord/projects/bot"
+ omdb: "http://omdbapi.com"
+ paste_service: "https://paste.pydis.com/{key}"