From e47715917e1367a85f79dd0500619b689a77f3bd Mon Sep 17 00:00:00 2001 From: jude Date: Mon, 20 Dec 2021 13:48:18 +0000 Subject: [PATCH] integrate reminder sender --- Cargo.lock | 253 ++++++++------- Cargo.toml | 2 +- README.md | 1 - src/commands/reminder_cmds.rs | 2 +- src/component_models/mod.rs | 3 +- src/consts.rs | 5 + src/main.rs | 62 +++- src/sender.rs | 557 ++++++++++++++++++++++++++++++++++ 8 files changed, 767 insertions(+), 118 deletions(-) create mode 100644 src/sender.rs diff --git a/Cargo.lock b/Cargo.lock index ac9fc11..e0d2a96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.51" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" dependencies = [ "proc-macro2", "quote", @@ -41,18 +41,18 @@ dependencies = [ [[package]] name = "async-tungstenite" -version = "0.15.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742cc7dcb20b2f84a42f4691aa999070ec7e78f8e7e7438bf14be7017b44907e" +checksum = "5682ea0913e5c20780fe5785abacb85a411e7437bf52a1bedb93ddb3972cb8dd" dependencies = [ "futures-io", "futures-util", "log", "pin-project-lite", "tokio", - "tokio-rustls", + "tokio-rustls 0.23.2", "tungstenite", - "webpki-roots", + "webpki-roots 0.22.1", ] [[package]] @@ -216,9 +216,9 @@ checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" [[package]] name = "crc32fast" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" dependencies = [ "cfg-if", ] @@ -255,12 +255,13 @@ dependencies = [ [[package]] name = "dashmap" -version = "4.0.2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +checksum = "b799062aaf67eb976af3bdca031ee6f846d2f0a5710ddbb0d2efee33f3cc4760" dependencies = [ "cfg-if", "num_cpus", + "parking_lot", "serde", ] @@ -287,9 +288,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "encoding_rs" -version = "0.8.29" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" dependencies = [ "cfg-if", ] @@ -397,19 +398,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" -[[package]] -name = "futures-macro" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" -dependencies = [ - "autocfg 1.0.1", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "futures-sink" version = "0.3.17" @@ -432,14 +420,11 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] @@ -477,9 +462,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" +checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd" dependencies = [ "bytes", "fnv", @@ -544,7 +529,7 @@ checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 0.4.8", ] [[package]] @@ -566,9 +551,9 @@ checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" [[package]] name = "httpdate" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "humantime" @@ -578,9 +563,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.14" +version = "0.14.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b" +checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" dependencies = [ "bytes", "futures-channel", @@ -591,7 +576,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 0.4.8", "pin-project-lite", "socket2", "tokio", @@ -602,17 +587,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" +checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" dependencies = [ - "futures-util", + "http", "hyper", - "log", - "rustls", + "rustls 0.20.2", "tokio", - "tokio-rustls", - "webpki", + "tokio-rustls 0.23.2", ] [[package]] @@ -666,9 +649,9 @@ checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" [[package]] name = "itertools" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" dependencies = [ "either", ] @@ -679,6 +662,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "js-sys" version = "0.3.55" @@ -705,9 +694,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.107" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" [[package]] name = "libm" @@ -920,9 +909,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] name = "opaque-debug" @@ -952,9 +941,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.70" +version = "0.9.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6517987b3f8226b5da3661dad65ff7f300cc59fb5ea8333ca191fc65fde3edf" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" dependencies = [ "autocfg 1.0.1", "cc", @@ -1028,9 +1017,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" [[package]] name = "ppv-lite86" @@ -1038,23 +1027,11 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro-nested" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" - [[package]] name = "proc-macro2" -version = "1.0.32" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1" dependencies = [ "unicode-xid", ] @@ -1187,7 +1164,7 @@ dependencies = [ [[package]] name = "reminder_rs" -version = "1.6.0-beta2" +version = "1.6.0-beta3" dependencies = [ "base64", "chrono", @@ -1223,9 +1200,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d2927ca2f685faf0fc620ac4834690d29e7abb153add10f5812eef20b5e280" +checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5" dependencies = [ "base64", "bytes", @@ -1246,18 +1223,20 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.20.2", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.23.2", + "tokio-util", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.21.1", "winreg", ] @@ -1326,15 +1305,36 @@ dependencies = [ "base64", "log", "ring", - "sct", - "webpki", + "sct 0.6.1", + "webpki 0.21.4", +] + +[[package]] +name = "rustls" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" +dependencies = [ + "log", + "ring", + "sct 0.7.0", + "webpki 0.22.0", +] + +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64", ] [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" [[package]] name = "schannel" @@ -1362,6 +1362,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "2.4.2" @@ -1387,18 +1397,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.130" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276" dependencies = [ "proc-macro2", "quote", @@ -1407,11 +1417,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.69" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8" +checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" dependencies = [ - "itoa", + "itoa 1.0.1", "ryu", "serde", ] @@ -1434,7 +1444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" dependencies = [ "form_urlencoded", - "itoa", + "itoa 0.4.8", "ryu", "serde", ] @@ -1442,7 +1452,7 @@ dependencies = [ [[package]] name = "serenity" version = "0.10.9" -source = "git+https://github.com/serenity-rs/serenity?branch=next#8d331b33171739ccecdc902faeefbcc7d32aa0eb" +source = "git+https://github.com/serenity-rs/serenity?branch=next#f349db848c16ec0b5f48afd63121573de22d800e" dependencies = [ "async-trait", "async-tungstenite", @@ -1590,7 +1600,7 @@ dependencies = [ "hashlink", "hex", "indexmap", - "itoa", + "itoa 0.4.8", "libc", "log", "memchr", @@ -1600,7 +1610,7 @@ dependencies = [ "percent-encoding", "rand 0.8.4", "rsa", - "rustls", + "rustls 0.19.1", "sha-1", "sha2", "smallvec", @@ -1610,8 +1620,8 @@ dependencies = [ "thiserror", "tokio-stream", "url", - "webpki", - "webpki-roots", + "webpki 0.21.4", + "webpki-roots 0.21.1", "whoami", ] @@ -1642,7 +1652,7 @@ checksum = "0d1bd069de53442e7a320f525a6d4deb8bb0621ac7a55f7eccbc2b58b57f43d0" dependencies = [ "once_cell", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", ] [[package]] @@ -1663,9 +1673,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" dependencies = [ "proc-macro2", "quote", @@ -1754,11 +1764,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" dependencies = [ - "autocfg 1.0.1", "bytes", "libc", "memchr", @@ -1774,9 +1783,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.5.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "114383b041aa6212c579467afa0075fbbdd0718de036100bc0ba7961d8cb9095" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", @@ -1799,9 +1808,20 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ - "rustls", + "rustls 0.19.1", "tokio", - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" +dependencies = [ + "rustls 0.20.2", + "tokio", + "webpki 0.22.0", ] [[package]] @@ -1876,9 +1896,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "tungstenite" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "983d40747bce878d2fb67d910dcb8bd3eca2b2358540c3cc1b98c027407a3ae3" +checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1" dependencies = [ "base64", "byteorder", @@ -1887,12 +1907,12 @@ dependencies = [ "httparse", "log", "rand 0.8.4", - "rustls", + "rustls 0.20.2", "sha-1", "thiserror", "url", "utf-8", - "webpki", + "webpki 0.22.0", ] [[package]] @@ -2103,20 +2123,39 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "webpki-roots" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "webpki-roots" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c475786c6f47219345717a043a37ec04cb4bc185e28853adcc4fa0a947eba630" +dependencies = [ + "webpki 0.22.0", ] [[package]] name = "whoami" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33ac5ee236a4efbf2c98967e12c6cc0c51d93a744159a52957ba206ae6ef5f7" +checksum = "524b58fa5a20a2fb3014dd6358b70e6579692a56ef6fce928834e488f42f65e8" dependencies = [ "wasm-bindgen", "web-sys", diff --git a/Cargo.toml b/Cargo.toml index 16cf289..beb2e09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reminder_rs" -version = "1.6.0-beta2" +version = "1.6.0-beta3" authors = ["jellywx "] edition = "2018" diff --git a/README.md b/README.md index bb8e49e..1b52b54 100644 --- a/README.md +++ b/README.md @@ -41,4 +41,3 @@ __Other Variables__ * Convert aliases to macros * Help command -* Test everything diff --git a/src/commands/reminder_cmds.rs b/src/commands/reminder_cmds.rs index 30475ae..ec4de66 100644 --- a/src/commands/reminder_cmds.rs +++ b/src/commands/reminder_cmds.rs @@ -322,7 +322,7 @@ async fn look(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) { .iter() .map(|reminder| reminder.display(&flags, &timezone)) .fold(0, |t, r| t + r.len()) - .div_ceil(EMBED_DESCRIPTION_MAX_LENGTH); + .div_ceil(&EMBED_DESCRIPTION_MAX_LENGTH); let pager = LookPager::new(flags, timezone); diff --git a/src/component_models/mod.rs b/src/component_models/mod.rs index bf16b2b..18e0c2e 100644 --- a/src/component_models/mod.rs +++ b/src/component_models/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod pager; use std::io::Cursor; use chrono_tz::Tz; +use num_integer::Integer; use rmp_serde::Serializer; use serde::{Deserialize, Serialize}; use serenity::{ @@ -78,7 +79,7 @@ impl ComponentDataModel { .iter() .map(|reminder| reminder.display(&flags, &pager.timezone)) .fold(0, |t, r| t + r.len()) - .div_ceil(EMBED_DESCRIPTION_MAX_LENGTH); + .div_ceil(&EMBED_DESCRIPTION_MAX_LENGTH); let channel_name = if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) { diff --git a/src/consts.rs b/src/consts.rs index cf9aa92..b36456f 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -14,6 +14,11 @@ use regex::Regex; use serenity::http::AttachmentType; lazy_static! { + pub static ref REMIND_INTERVAL: u64 = env::var("REMIND_INTERVAL") + .map(|inner| inner.parse::().ok()) + .ok() + .flatten() + .unwrap_or(10); pub static ref DEFAULT_AVATAR: AttachmentType<'static> = ( include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), diff --git a/src/main.rs b/src/main.rs index 2a23a58..53c4abe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,9 +8,17 @@ mod consts; mod framework; mod hooks; mod models; +mod sender; mod time_parser; -use std::{collections::HashMap, env, sync::Arc}; +use std::{ + collections::HashMap, + env, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; use chrono_tz::Tz; use dotenv::dotenv; @@ -30,12 +38,15 @@ use serenity::{ utils::shard_id, }; use sqlx::mysql::MySqlPool; -use tokio::sync::RwLock; +use tokio::{ + sync::RwLock, + time::{Duration, Instant}, +}; use crate::{ commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds}, component_models::ComponentDataModel, - consts::{CNC_GUILD, SUBSCRIPTION_ROLES, THEME_COLOR}, + consts::{CNC_GUILD, SUBSCRIPTION_ROLES, THEME_COLOR, REMIND_INTERVAL}, framework::RegexFramework, models::command_macro::CommandMacro, }; @@ -64,10 +75,45 @@ impl TypeMapKey for RecordingMacros { type Value = Arc>>; } -struct Handler; +struct Handler { + is_loop_running: AtomicBool, +} #[async_trait] impl EventHandler for Handler { + async fn cache_ready(&self, ctx_base: Context, _guilds: Vec) { + info!("Cache Ready!"); + info!("Preparing to send reminders"); + + if !self.is_loop_running.load(Ordering::Relaxed) { + let ctx = ctx_base.clone(); + + tokio::spawn(async move { + let pool = ctx.data.read().await.get::().cloned().unwrap(); + + loop { + let sleep_until = Instant::now() + Duration::from_secs(*REMIND_INTERVAL); + let reminders = sender::Reminder::fetch_reminders(&pool).await; + + if reminders.len() > 0 { + info!("================================================="); + info!("Preparing to send {} reminders:", reminders.len()); + + for reminder in reminders { + info!("Sending {:?}", reminder); + + reminder.send(pool.clone(), ctx.clone()).await; + } + } + + tokio::time::sleep_until(sleep_until).await; + } + }); + + self.is_loop_running.swap(true, Ordering::Relaxed); + } + } + async fn channel_delete(&self, ctx: Context, channel: &GuildChannel) { let pool = ctx .data @@ -185,9 +231,11 @@ async fn main() -> Result<(), Box> { let token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment"); - let http = Http::new_with_token(&token); + let application_id = { + let http = Http::new_with_token(&token); - let application_id = http.get_current_application_info().await?.id; + http.get_current_application_info().await?.id + }; let dm_enabled = env::var("DM_ENABLED").map_or(true, |var| var == "1"); @@ -225,7 +273,7 @@ async fn main() -> Result<(), Box> { let mut client = Client::builder(&token) .intents(GatewayIntents::GUILDS) .application_id(application_id.0) - .event_handler(Handler) + .event_handler(Handler { is_loop_running: AtomicBool::from(false) }) .await .expect("Error occurred creating client"); diff --git a/src/sender.rs b/src/sender.rs new file mode 100644 index 0000000..7bace4c --- /dev/null +++ b/src/sender.rs @@ -0,0 +1,557 @@ +use chrono::Duration; +use log::{error, info, warn}; +use serenity::{ + builder::CreateEmbed, + http::{CacheHttp, Http, StatusCode}, + model::{channel::Embed as SerenityEmbed, id::ChannelId, webhook::Webhook}, + Error, Result, +}; +use sqlx::{ + types::chrono::{NaiveDateTime, Utc}, + MySqlPool, +}; + +use chrono_tz::Tz; +use num_integer::Integer; +use regex::{Captures, Regex}; +use serenity::model::channel::Channel; + +lazy_static! { + pub static ref TIMEFROM_REGEX: Regex = + Regex::new(r#"<\d+):(?P.+)?>>"#).unwrap(); + pub static ref TIMENOW_REGEX: Regex = + Regex::new(r#"<(?:\w|/|_)+):(?P.+)?>>"#).unwrap(); +} + +fn fmt_displacement(format: &str, seconds: u64) -> String { + let mut seconds = seconds; + let mut days: u64 = 0; + let mut hours: u64 = 0; + let mut minutes: u64 = 0; + + for (rep, time_type, div) in [ + ("%d", &mut days, 86400), + ("%h", &mut hours, 3600), + ("%m", &mut minutes, 60), + ] + .iter_mut() + { + if format.contains(*rep) { + let (divided, new_seconds) = seconds.div_rem(&div); + + **time_type = divided; + seconds = new_seconds; + } + } + + format + .replace("%s", &seconds.to_string()) + .replace("%m", &minutes.to_string()) + .replace("%h", &hours.to_string()) + .replace("%d", &days.to_string()) +} + +pub fn substitute(string: &str) -> String { + let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| { + let final_time = caps.name("time").unwrap().as_str(); + let format = caps.name("format").unwrap().as_str(); + + if let Ok(final_time) = final_time.parse::() { + let dt = NaiveDateTime::from_timestamp(final_time, 0); + let now = Utc::now().naive_utc(); + + let difference = { + if now < dt { + dt - Utc::now().naive_utc() + } else { + Utc::now().naive_utc() - dt + } + }; + + fmt_displacement(format, difference.num_seconds() as u64) + } else { + String::new() + } + }); + + TIMENOW_REGEX + .replace(&new, |caps: &Captures| { + let timezone = caps.name("timezone").unwrap().as_str(); + + println!("{}", timezone); + + if let Ok(tz) = timezone.parse::() { + let format = caps.name("format").unwrap().as_str(); + let now = Utc::now().with_timezone(&tz); + + now.format(format).to_string() + } else { + String::new() + } + }) + .to_string() +} + +struct Embed { + inner: EmbedInner, + fields: Vec, +} + +struct EmbedInner { + title: String, + description: String, + image_url: Option, + thumbnail_url: Option, + footer: String, + footer_url: Option, + author: String, + author_url: Option, + color: u32, +} + +struct EmbedField { + title: String, + value: String, + inline: bool, +} + +impl Embed { + pub async fn from_id(pool: &MySqlPool, id: u32) -> Option { + let mut inner = sqlx::query_as_unchecked!( + EmbedInner, + " +SELECT + `embed_title` AS title, + `embed_description` AS description, + `embed_image_url` AS image_url, + `embed_thumbnail_url` AS thumbnail_url, + `embed_footer` AS footer, + `embed_footer_url` AS footer_url, + `embed_author` AS author, + `embed_author_url` AS author_url, + `embed_color` AS color +FROM + reminders +WHERE + `id` = ? + ", + id + ) + .fetch_one(&pool.clone()) + .await + .unwrap(); + + inner.title = substitute(&inner.title); + inner.description = substitute(&inner.description); + inner.footer = substitute(&inner.footer); + + let mut fields = sqlx::query_as_unchecked!( + EmbedField, + " +SELECT + title, + value, + inline +FROM + embed_fields +WHERE + reminder_id = ? + ", + id + ) + .fetch_all(pool) + .await + .unwrap(); + + fields.iter_mut().for_each(|mut field| { + field.title = substitute(&field.title); + field.value = substitute(&field.value); + }); + + let e = Embed { inner, fields }; + + if e.has_content() { + Some(e) + } else { + None + } + } + + pub fn has_content(&self) -> bool { + if self.inner.title.is_empty() + && self.inner.description.is_empty() + && self.inner.image_url.is_none() + && self.inner.thumbnail_url.is_none() + && self.inner.footer.is_empty() + && self.inner.footer_url.is_none() + && self.inner.author.is_empty() + && self.inner.author_url.is_none() + && self.fields.is_empty() + { + false + } else { + true + } + } +} + +impl Into for Embed { + fn into(self) -> CreateEmbed { + let mut c = CreateEmbed::default(); + + c.title(&self.inner.title) + .description(&self.inner.description) + .color(self.inner.color) + .author(|a| { + a.name(&self.inner.author); + + if let Some(author_icon) = &self.inner.author_url { + a.icon_url(author_icon); + } + + a + }) + .footer(|f| { + f.text(&self.inner.footer); + + if let Some(footer_icon) = &self.inner.footer_url { + f.icon_url(footer_icon); + } + + f + }); + + for field in &self.fields { + c.field(&field.title, &field.value, field.inline); + } + + if let Some(image_url) = &self.inner.image_url { + c.image(image_url); + } + + if let Some(thumbnail_url) = &self.inner.thumbnail_url { + c.thumbnail(thumbnail_url); + } + + c + } +} + +#[derive(Debug)] +pub struct Reminder { + id: u32, + + channel_id: u64, + webhook_id: Option, + webhook_token: Option, + + channel_paused: bool, + channel_paused_until: Option, + enabled: bool, + + tts: bool, + pin: bool, + content: String, + attachment: Option>, + attachment_name: Option, + + utc_time: NaiveDateTime, + timezone: String, + restartable: bool, + expires: Option, + interval: Option, + + avatar: Option, + username: Option, +} + +impl Reminder { + pub async fn fetch_reminders(pool: &MySqlPool) -> Vec { + sqlx::query_as_unchecked!( + Reminder, + " +SELECT + reminders.`id` AS id, + + channels.`channel` AS channel_id, + channels.`webhook_id` AS webhook_id, + channels.`webhook_token` AS webhook_token, + + channels.`paused` AS channel_paused, + channels.`paused_until` AS channel_paused_until, + reminders.`enabled` AS enabled, + + reminders.`tts` AS tts, + reminders.`pin` AS pin, + reminders.`content` AS content, + reminders.`attachment` AS attachment, + reminders.`attachment_name` AS attachment_name, + + reminders.`utc_time` AS 'utc_time', + reminders.`timezone` AS timezone, + reminders.`restartable` AS restartable, + reminders.`expires` AS expires, + reminders.`interval` AS 'interval', + + reminders.`avatar` AS avatar, + reminders.`username` AS username +FROM + reminders +INNER JOIN + channels +ON + reminders.channel_id = channels.id +WHERE + reminders.`utc_time` < NOW() + ", + ) + .fetch_all(pool) + .await + .unwrap() + .into_iter() + .map(|mut rem| { + rem.content = substitute(&rem.content); + + rem + }) + .collect::>() + } + + async fn reset_webhook(&self, pool: &MySqlPool) { + let _ = sqlx::query!( + " +UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ? + ", + self.channel_id + ) + .execute(pool) + .await; + } + + async fn refresh(&self, pool: &MySqlPool) { + if let Some(interval) = self.interval { + let now = Utc::now().naive_local(); + let mut updated_reminder_time = self.utc_time; + + while updated_reminder_time < now { + updated_reminder_time += Duration::seconds(interval as i64); + } + + if self.expires.map_or(false, |expires| { + NaiveDateTime::from_timestamp(updated_reminder_time.timestamp(), 0) > expires + }) { + self.force_delete(pool).await; + } else { + sqlx::query!( + " +UPDATE reminders SET `utc_time` = ? WHERE `id` = ? + ", + updated_reminder_time, + self.id + ) + .execute(pool) + .await + .expect(&format!("Could not update time on Reminder {}", self.id)); + } + } else { + self.force_delete(pool).await; + } + } + + async fn force_delete(&self, pool: &MySqlPool) { + sqlx::query!( + " +DELETE FROM reminders WHERE `id` = ? + ", + self.id + ) + .execute(pool) + .await + .expect(&format!("Could not delete Reminder {}", self.id)); + } + + async fn pin_message>(&self, message_id: M, http: impl AsRef) { + let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await; + } + + pub async fn send(&self, pool: MySqlPool, cache_http: impl CacheHttp) { + async fn send_to_channel( + cache_http: impl CacheHttp, + reminder: &Reminder, + embed: Option, + ) -> Result<()> { + let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await; + + match channel { + Ok(Channel::Guild(channel)) => { + match channel + .send_message(&cache_http, |m| { + m.content(&reminder.content).tts(reminder.tts); + + if let (Some(attachment), Some(name)) = + (&reminder.attachment, &reminder.attachment_name) + { + m.add_file((attachment as &[u8], name.as_str())); + } + + if let Some(embed) = embed { + m.set_embed(embed); + } + + m + }) + .await + { + Ok(m) => { + if reminder.pin { + reminder.pin_message(m.id, cache_http.http()).await; + } + + Ok(()) + } + Err(e) => Err(e), + } + } + Ok(Channel::Private(channel)) => { + match channel + .send_message(&cache_http.http(), |m| { + m.content(&reminder.content).tts(reminder.tts); + + if let (Some(attachment), Some(name)) = + (&reminder.attachment, &reminder.attachment_name) + { + m.add_file((attachment as &[u8], name.as_str())); + } + + if let Some(embed) = embed { + m.set_embed(embed); + } + + m + }) + .await + { + Ok(m) => { + if reminder.pin { + reminder.pin_message(m.id, cache_http.http()).await; + } + + Ok(()) + } + Err(e) => Err(e), + } + } + Err(e) => { + Err(e) + } + _ => { + Err(Error::Other("Channel not of valid type")) + } + } + } + + async fn send_to_webhook( + cache_http: impl CacheHttp, + reminder: &Reminder, + webhook: Webhook, + embed: Option, + ) -> Result<()> { + match webhook + .execute(&cache_http.http(), reminder.pin || reminder.restartable, |w| { + w.content(&reminder.content).tts(reminder.tts); + + if let Some(username) = &reminder.username { + w.username(username); + } + + if let Some(avatar) = &reminder.avatar { + w.avatar_url(avatar); + } + + if let (Some(attachment), Some(name)) = + (&reminder.attachment, &reminder.attachment_name) + { + w.add_file((attachment as &[u8], name.as_str())); + } + + if let Some(embed) = embed { + w.embeds(vec![SerenityEmbed::fake(|c| { + *c = embed; + c + })]); + } + + w + }) + .await + { + Ok(m) => { + if reminder.pin { + if let Some(message) = m { + reminder.pin_message(message.id, cache_http.http()).await; + } + } + + Ok(()) + } + Err(e) => Err(e), + } + } + + if self.enabled + && !(self.channel_paused + && self + .channel_paused_until + .map_or(true, |inner| inner >= Utc::now().naive_local())) + { + let _ = sqlx::query!( + " +UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ? + ", + self.channel_id + ) + .execute(&pool.clone()) + .await; + + let embed = Embed::from_id(&pool.clone(), self.id).await.map(|e| e.into()); + + let result = if let (Some(webhook_id), Some(webhook_token)) = + (self.webhook_id, &self.webhook_token) + { + let webhook_res = cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await; + + if let Ok(webhook) = webhook_res { + send_to_webhook(cache_http, &self, webhook, embed).await + } else { + warn!("Webhook vanished: {:?}", webhook_res); + + self.reset_webhook(&pool.clone()).await; + send_to_channel(cache_http, &self, embed).await + } + } else { + send_to_channel(cache_http, &self, embed).await + }; + + if let Err(e) = result { + error!("Error sending {:?}: {:?}", self, e); + + if let Error::Http(error) = e { + if error.status_code() == Some(StatusCode::from_u16(404).unwrap()) { + error!("Seeing channel is deleted. Removing reminder"); + self.force_delete(&pool).await; + } else { + self.refresh(&pool).await; + } + } else { + self.refresh(&pool).await; + } + } else { + self.refresh(&pool).await; + } + } else { + info!("Reminder {} is paused", self.id); + + self.refresh(&pool).await; + } + } +}