diff --git a/.gitignore b/.gitignore index 307cddf..2c7a7fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target .env *.yaml cargo-remote +.vscode diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 8f7f2e1..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'youmubot-osu'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=youmubot-osu" - ], - "filter": { - "name": "youmubot-osu", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'youmubot'", - "cargo": { - "args": [ - "build", - "--bin=youmubot", - "--package=youmubot" - ], - "filter": { - "name": "youmubot", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in executable 'youmubot'", - "cargo": { - "args": [ - "test", - "--no-run", - "--bin=youmubot", - "--package=youmubot" - ], - "filter": { - "name": "youmubot", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 740b93b..aa6c2ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,1979 +4,1934 @@ name = "Inflector" version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "regex", ] [[package]] -name = "addr2line" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "gimli 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "adler32" -version = "1.0.4" +name = "adler" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" [[package]] name = "ahash" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" dependencies = [ - "const-random 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "const-random", ] [[package]] name = "aho-corasick" -version = "0.7.10" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" dependencies = [ - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" + +[[package]] +name = "arc-swap" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" + +[[package]] +name = "async-tls" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7e7fbc0843fc5ad3d5ca889c5b2bea9130984d34cd0e62db57ab70c2529a8e3" +dependencies = [ + "futures", + "rustls", + "webpki", + "webpki-roots 0.20.0", +] + +[[package]] +name = "async-trait" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687c230d85c0a52504709705fc8a53e4a692b83a2184f03dae73e38e1e93a783" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-tungstenite" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5c45a0dd44b7e6533ac4e7acc38ead1a3b39885f5bbb738140d30ea528abc7c" +dependencies = [ + "async-tls", + "futures-io", + "futures-util", + "log", + "pin-project", + "tokio", + "tungstenite", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", ] [[package]] name = "autocfg" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "backtrace" -version = "0.3.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "addr2line 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "object 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "base64" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "base64" -version = "0.11.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "block-buffer" -version = "0.7.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array", ] [[package]] name = "bumpalo" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "byte-tools" -version = "0.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" [[package]] name = "byteorder" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" [[package]] name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "bytes" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "cc" -version = "1.0.54" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381" [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "chrono" -version = "0.4.11" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" dependencies = [ - "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer", + "num-traits", + "serde", + "time", ] [[package]] name = "codeforces" -version = "0.1.0" -source = "git+https://github.com/natsukagami/rust-codeforces-api#e10f2155df238fe5edd2f0d33cb8d6a4ce252e69" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07443ebea310b4bbfd0c9ee79d25cf678fa1e7c82916bb32307af7a7ef1fe600" dependencies = [ - "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-timer", + "futures-util", + "reqwest", + "serde", + "serde_json", ] [[package]] name = "command_attr" -version = "0.2.0" +version = "0.3.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c538daab2daaf13de61cea91648a62bb11d267ef629f707d5fe3dd080043ab4d" dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "const-random" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a" dependencies = [ - "const-random-macro 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", + "const-random-macro", + "proc-macro-hack", ] [[package]] name = "const-random-macro" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a" dependencies = [ - "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", + "proc-macro-hack", ] [[package]] name = "core-foundation" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" dependencies = [ - "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys", + "libc", ] [[package]] name = "core-foundation-sys" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" [[package]] name = "crc32fast" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-channel" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-deque" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-epoch 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memoffset 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-queue" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "crossbeam-utils" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "ct-logs" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", ] [[package]] name = "dashmap" -version = "3.11.4" +version = "3.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f260e2fc850179ef410018660006951c1b55b79e8087e87111a2c388994b9b5" dependencies = [ - "ahash 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ahash", + "cfg-if", + "num_cpus", ] [[package]] name = "digest" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array", ] [[package]] name = "dotenv" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "dtoa" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "either" -version = "1.5.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" [[package]] name = "encoding_rs" -version = "0.8.23" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a51b8cf747471cb9499b6d59e59b0444f4c90eba8968c4e44874e92b5b64ace2" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", ] [[package]] -name = "failure" -version = "0.1.8" +name = "env_logger" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ - "backtrace 0.3.48 (registry+https://github.com/rust-lang/crates.io-index)", - "failure_derive 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "atty", + "humantime", + "log", + "regex", + "termcolor", ] -[[package]] -name = "failure_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", - "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "flate2" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "766d0e77a2c1502169d4a93ff3b8c15a71fd946cd0126309752104e5f3c46d94" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f7d4f1d85cb8cafb73c01d872e0124a96e603f5b7633ce1eac70015f066e946" +dependencies = [ + "futures", + "rand", + "spinning_top", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "fuchsia-zircon" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "fuchsia-zircon-sys", ] [[package]] name = "fuchsia-zircon-sys" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] [[package]] name = "futures-channel" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" dependencies = [ - "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core", + "futures-sink", ] [[package]] name = "futures-core" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" + +[[package]] +name = "futures-executor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" [[package]] name = "futures-macro" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" dependencies = [ - "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "futures-sink" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" [[package]] name = "futures-task" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" dependencies = [ - "once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "once_cell", ] +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" dependencies = [ - "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-io 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-nested 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", ] [[package]] name = "generic-array" -version = "0.12.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" dependencies = [ - "typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "typenum", + "version_check", ] [[package]] name = "getrandom" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", ] -[[package]] -name = "gimli" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "h2" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] +[[package]] +name = "hashbrown" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7" + [[package]] name = "hermit-abi" -version = "0.1.13" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" dependencies = [ - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "http" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] name = "http" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes", + "fnv", + "itoa", ] [[package]] name = "http-body" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes", + "http", ] [[package]] name = "httparse" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] [[package]] name = "hyper" -version = "0.13.5" +version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e68a8dd9716185d9e64ea473ea6ef63529252e3e27623295a0378a19665d5eb" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "h2 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project", + "socket2", + "time", + "tokio", + "tower-service", + "tracing", + "want", ] [[package]] name = "hyper-rustls" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37743cc83e8ee85eacfce90f2f4102030d9ff0a95244098d781e9bee4a90abb6" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "ct-logs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rustls-native-certs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", - "webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes", + "futures-util", + "hyper", + "log", + "rustls", + "tokio", + "tokio-rustls", + "webpki", ] [[package]] name = "hyper-tls" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-tls 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-tls", ] [[package]] name = "idna" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "matches", + "unicode-bidi", + "unicode-normalization", ] [[package]] name = "indexmap" -version = "1.3.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", + "hashbrown", ] [[package]] name = "input_buffer" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a8a95243d5a0398cae618ec29477c6e3cb631152be5c19481f80bc71559754" dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes", ] [[package]] name = "iovec" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" dependencies = [ - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] -name = "itoa" -version = "0.4.5" +name = "ipnet" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" [[package]] name = "js-sys" -version = "0.3.39" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8" dependencies = [ - "wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen", ] [[package]] name = "kernel32-sys" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8", + "winapi-build", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.70" +version = "0.2.77" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" [[package]] name = "linked-hash-map" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" [[package]] name = "lock_api" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" dependencies = [ - "scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "scopeguard", ] [[package]] name = "log" -version = "0.4.8" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", ] [[package]] name = "matches" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" [[package]] name = "memchr" version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "memoffset" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" [[package]] name = "mime" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mime_guess" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" dependencies = [ - "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "mime", + "unicase", ] [[package]] name = "miniz_oxide" -version = "0.3.6" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60c0dfe32c10b43a144bad8fc83538c52f58302c92300ea7ec7bf7b38d5a7b9" dependencies = [ - "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "adler", + "autocfg", ] [[package]] name = "mio" version = "0.6.22" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.1", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-named-pipes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +dependencies = [ + "log", + "mio", + "miow 0.3.5", + "winapi 0.3.9", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", ] [[package]] name = "miow" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "net2 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b88fb9795d4d36d62a012dfbf49a8f5cf12751f36d31a9dbe66d528e58979e" +dependencies = [ + "socket2", + "winapi 0.3.9", ] [[package]] name = "native-tls" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl 0.10.29 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.9.56 (registry+https://github.com/rust-lang/crates.io-index)", - "schannel 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", - "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", ] [[package]] name = "net2" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ebc3ec692ed7c9a255596c67808dee269f64655d8baf7b4f0638e51ba1d6853" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "libc", + "winapi 0.3.9", ] [[package]] name = "num-integer" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", + "num-traits", ] [[package]] name = "num-traits" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", ] [[package]] name = "num_cpus" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" dependencies = [ - "hermit-abi 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", + "hermit-abi", + "libc", ] -[[package]] -name = "object" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "once_cell" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" [[package]] name = "opaque-debug" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.29" +version = "0.10.30" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "openssl-sys 0.9.56 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "cfg-if", + "foreign-types", + "lazy_static", + "libc", + "openssl-sys", ] [[package]] name = "openssl-probe" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-sys" -version = "0.9.56" +version = "0.9.58" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", - "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", ] [[package]] name = "oppai-rs" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f143357550da5c04800333509df440fcbe5254120a5af05097a083caf23105b" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "parking_lot" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "lock_api 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "parking_lot_core" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "cc", + "libc", ] [[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project" -version = "0.4.17" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" dependencies = [ - "pin-project-internal 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.17" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "pin-project-lite" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" [[package]] name = "ppv-lite86" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" [[package]] name = "proc-macro-hack" -version = "0.5.15" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" [[package]] name = "proc-macro-nested" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" [[package]] name = "proc-macro2" -version = "1.0.14" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e28516df94f3dd551a587da5357459d9b36d945a7c37c3557928c1c2ff2a2c" dependencies = [ - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid", ] [[package]] -name = "quote" -version = "1.0.6" +name = "quick-error" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", ] [[package]] name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", ] [[package]] name = "rand_chacha" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ - "ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ppv-lite86", + "rand_core", ] [[package]] name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "getrandom", ] [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rayon" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-deque 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rayon-core" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "crossbeam-deque 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core", ] [[package]] name = "redox_syscall" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "regex" -version = "1.3.7" +version = "1.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" dependencies = [ - "aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)", - "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", ] [[package]] name = "regex-syntax" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" [[package]] name = "remove_dir_all" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9", ] [[package]] name = "reqwest" -version = "0.10.4" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9eaa17ac5d7b838b7503d118fa16ad88f440498bf9ffe5424e621f93190d61e" dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "encoding_rs 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper-rustls 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", - "hyper-tls 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", - "mime_guess 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project-lite 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-tls 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-futures 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", - "webpki-roots 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "tokio-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.19.0", + "winreg", ] [[package]] name = "ring" -version = "0.16.13" +version = "0.16.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4" dependencies = [ - "cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", ] [[package]] name = "rustbreak" -version = "2.0.0-rc3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460d97902465327d69ecfe8cefdb5972c6f94d6127ac9e992acdb51458bebc27" dependencies = [ - "failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_yaml 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", + "serde_yaml", + "tempfile", + "thiserror", ] [[package]] name = "rustls" -version = "0.16.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" dependencies = [ - "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "ring 0.16.13 (registry+https://github.com/rust-lang/crates.io-index)", - "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rustls" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "ring 0.16.13 (registry+https://github.com/rust-lang/crates.io-index)", - "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rustls-native-certs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", - "schannel 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "base64", + "log", + "ring", + "sct", + "webpki", ] [[package]] name = "ryu" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" [[package]] name = "schannel" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "winapi 0.3.9", ] [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "sct" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" dependencies = [ - "ring 0.16.13 (registry+https://github.com/rust-lang/crates.io-index)", - "untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ring", + "untrusted", ] [[package]] name = "security-framework" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" dependencies = [ - "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys", + "libc", ] -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "serde" -version = "1.0.110" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" dependencies = [ - "serde_derive 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.110" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "serde_json" -version = "1.0.53" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" dependencies = [ - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "ryu 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa", + "ryu", + "serde", ] [[package]] name = "serde_urlencoded" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" dependencies = [ - "dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "dtoa", + "itoa", + "serde", + "url", ] [[package]] name = "serde_yaml" -version = "0.7.5" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3e2dd40a7cdc18ca80db804b7f461a39bb721160a85c9a1fa30134bf3c02a5" dependencies = [ - "dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", - "linked-hash-map 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", ] [[package]] name = "serenity" -version = "0.8.6" +version = "0.9.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e935a7f3f4752257183ee1f3553b10ea5b514a55de0e536dca7f3742b97d18" dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "command_attr 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "flate2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rustls 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", - "static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "threadpool 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tungstenite 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", - "typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "uwl 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)", - "webpki-roots 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", + "async-trait", + "async-tungstenite", + "base64", + "bitflags", + "bytes", + "chrono", + "command_attr", + "flate2", + "futures", + "log", + "reqwest", + "serde", + "serde_json", + "static_assertions", + "tokio", + "typemap_rev", + "url", + "uwl", ] [[package]] name = "sha-1" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170a36ea86c864a3f16dd2687712dd6646f7019f301e57537c7f4dc9f5916770" dependencies = [ - "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "block-buffer", + "cfg-if", + "cpuid-bool", + "digest", + "opaque-debug", +] + +[[package]] +name = "signal-hook-registry" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e12110bc539e657a646068aaf5eb5b63af9d0c1f7b29c97113fad80e15f035" +dependencies = [ + "arc-swap", + "libc", ] [[package]] name = "slab" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] -name = "smallvec" -version = "0.6.13" +name = "socket2" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1fa70dc5c8104ec096f4fe7ede7a221d35ae13dcd19ba1ad9a81d2cab9a1c44" dependencies = [ - "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.9", ] -[[package]] -name = "smallvec" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spinning_top" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e529d73e80d64b5f2631f9035113347c578a1c9c7774b83a2b880788459ab36" +dependencies = [ + "lock_api", +] [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "1.0.23" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963f7d3cc59b59b9325165add223142bbf1df27655d07789f109896d353d8350" dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "synstructure" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "unicode-xid", ] [[package]] name = "tempfile" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "thread_local" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", ] [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ - "libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", ] [[package]] -name = "tokio" -version = "0.2.21" +name = "tinyvec" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117" + +[[package]] +name = "tokio" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d34ca54d84bf2b5b4d7d31e901a8464f7b60ac145a284fba25ceb801f2ddccd" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.22 (registry+https://github.com/rust-lang/crates.io-index)", - "num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project-lite 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-named-pipes", + "mio-uds", + "pin-project-lite", + "signal-hook-registry", + "slab", + "tokio-macros", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-macros" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "tokio-rustls" -version = "0.13.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a" dependencies = [ - "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-core", + "rustls", + "tokio", + "webpki", ] [[package]] name = "tokio-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" dependencies = [ - "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "native-tls", + "tokio", ] [[package]] name = "tokio-util" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" dependencies = [ - "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "pin-project-lite 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", ] [[package]] name = "tower-service" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] -name = "traitobject" -version = "0.1.0" +name = "tracing" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" +dependencies = [ + "cfg-if", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcf46c1f1f06aeea2d6b81f3c863d0930a596c86ad1920d4e5bad6dd1d7119a" +dependencies = [ + "lazy_static", +] [[package]] name = "try-lock" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "tungstenite" -version = "0.9.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0308d80d86700c5878b9ef6321f020f29b1bb9d5ff3cab25e75e23f3a492a23" dependencies = [ - "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", - "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "input_buffer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", - "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "utf-8 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)", + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "input_buffer", + "log", + "rand", + "sha-1", + "url", + "utf-8", ] [[package]] -name = "typemap" -version = "0.3.3" +name = "typemap_rev" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "078d41124321488746becfa144977b9b54667af408ff933cbbce9d83e7796ac9" [[package]] name = "typenum" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" [[package]] name = "unicase" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" dependencies = [ - "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check", ] [[package]] name = "unicode-bidi" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" dependencies = [ - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "matches", ] [[package]] name = "unicode-normalization" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" dependencies = [ - "smallvec 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tinyvec", ] [[package]] name = "unicode-xid" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "unsafe-any" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" dependencies = [ - "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", - "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "idna", + "matches", + "percent-encoding", ] [[package]] name = "utf-8" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" [[package]] name = "uwl" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0" [[package]] name = "vcpkg" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" [[package]] name = "version_check" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" [[package]] name = "want" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" dependencies = [ - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log", + "try-lock", ] [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.62" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "serde", + "serde_json", + "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.62" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68" dependencies = [ - "bumpalo 3.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.12" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7866cab0aa01de1edf8b5d7936938a7e397ee50ce24119aef3e1eaa3b6171da" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", - "js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "web-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.62" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" dependencies = [ - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-macro-support 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "quote", + "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.62" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-backend 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen-shared 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.62" +version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" [[package]] name = "web-sys" -version = "0.3.39" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d" dependencies = [ - "js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)", - "wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "js-sys", + "wasm-bindgen", ] [[package]] name = "webpki" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae" dependencies = [ - "ring 0.16.13 (registry+https://github.com/rust-lang/crates.io-index)", - "untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "ring", + "untrusted", ] [[package]] name = "webpki-roots" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739" dependencies = [ - "webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki", +] + +[[package]] +name = "webpki-roots" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" +dependencies = [ + "webpki", ] [[package]] name = "winapi" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" [[package]] name = "winapi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-build" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winreg" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9", ] [[package]] name = "ws2_32-sys" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8", + "winapi-build", ] [[package]] name = "yaml-rust" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" dependencies = [ - "linked-hash-map 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "linked-hash-map", ] [[package]] name = "youmubot" version = "0.1.0" dependencies = [ - "dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serenity 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", - "youmubot-cf 0.1.0", - "youmubot-core 0.1.0", - "youmubot-db 0.1.0", - "youmubot-osu 0.1.0", - "youmubot-prelude 0.1.0", + "dotenv", + "env_logger", + "serenity", + "tokio", + "youmubot-cf", + "youmubot-core", + "youmubot-db", + "youmubot-osu", + "youmubot-prelude", ] [[package]] name = "youmubot-cf" version = "0.1.0" dependencies = [ - "Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)", - "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "codeforces 0.1.0 (git+https://github.com/natsukagami/rust-codeforces-api)", - "crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "serenity 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", - "youmubot-db 0.1.0", - "youmubot-prelude 0.1.0", + "Inflector", + "chrono", + "codeforces", + "dashmap", + "lazy_static", + "regex", + "reqwest", + "serde", + "serenity", + "tokio", + "youmubot-db", + "youmubot-prelude", ] [[package]] name = "youmubot-core" version = "0.1.0" dependencies = [ - "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "serenity 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", - "static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "youmubot-db 0.1.0", - "youmubot-prelude 0.1.0", + "chrono", + "futures-util", + "rand", + "serde", + "serenity", + "static_assertions", + "tokio", + "youmubot-db", + "youmubot-prelude", ] [[package]] name = "youmubot-db" version = "0.1.0" dependencies = [ - "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rustbreak 2.0.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "serenity 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono", + "dotenv", + "rustbreak", + "serde", + "serenity", ] [[package]] name = "youmubot-osu" version = "0.1.0" dependencies = [ - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "dashmap 3.11.4 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "oppai-rs 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", - "serenity 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", - "youmubot-db 0.1.0", - "youmubot-prelude 0.1.0", + "bitflags", + "chrono", + "dashmap", + "lazy_static", + "oppai-rs", + "regex", + "reqwest", + "serde", + "serde_json", + "serenity", + "youmubot-db", + "youmubot-prelude", ] [[package]] name = "youmubot-prelude" version = "0.1.0" dependencies = [ - "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", - "serenity 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", - "youmubot-db 0.1.0", + "anyhow", + "async-trait", + "chrono", + "flume", + "futures-util", + "reqwest", + "serenity", + "tokio", + "youmubot-db", ] - -[metadata] -"checksum Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)" = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -"checksum addr2line 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a49806b9dadc843c61e7c97e72490ad7f7220ae249012fbda9ad0609457c0543" -"checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" -"checksum ahash 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" -"checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" -"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" -"checksum backtrace 0.3.48 (registry+https://github.com/rust-lang/crates.io-index)" = "0df2f85c8a2abbe3b7d7e748052fdd9b76a0458fdeb16ad4223f5eca78c7c130" -"checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" -"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" -"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -"checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -"checksum bumpalo 3.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5356f1d23ee24a1f785a56d1d1a5f0fd5b0f6a0c0fb2412ce11da71649ab78f6" -"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" -"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" -"checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -"checksum bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" -"checksum cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)" = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311" -"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -"checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" -"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -"checksum codeforces 0.1.0 (git+https://github.com/natsukagami/rust-codeforces-api)" = "" -"checksum command_attr 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c27d6155f93d880b6379d93ddc9b2417b3b69b715360c5f25525e4576338a381" -"checksum const-random 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a" -"checksum const-random-macro 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a" -"checksum core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -"checksum core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" -"checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" -"checksum crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" -"checksum crossbeam-deque 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" -"checksum crossbeam-epoch 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" -"checksum crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" -"checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -"checksum ct-logs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" -"checksum dashmap 3.11.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8cfcd41ae02d60edded204341d2798ba519c336c51a37330aa4b98a1128def32" -"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -"checksum dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" -"checksum dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" -"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" -"checksum encoding_rs 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171" -"checksum failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" -"checksum failure_derive 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" -"checksum flate2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" -"checksum fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" -"checksum futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" -"checksum futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" -"checksum futures-io 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" -"checksum futures-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" -"checksum futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" -"checksum futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" -"checksum futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" -"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" -"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" -"checksum gimli 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c" -"checksum h2 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff" -"checksum hermit-abi 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71" -"checksum http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" -"checksum http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" -"checksum http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" -"checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" -"checksum hyper 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96816e1d921eca64d208a85aab4f7798455a8e34229ee5a88c935bdee1b78b14" -"checksum hyper-rustls 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac965ea399ec3a25ac7d13b8affd4b8f39325cca00858ddf5eb29b79e6b14b08" -"checksum hyper-tls 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3adcd308402b9553630734e9c36b77a7e48b3821251ca2493e8cd596763aafaa" -"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" -"checksum indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" -"checksum input_buffer 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8e1b822cc844905551931d6f81608ed5f50a79c1078a4e2b4d42dbc7c1eedfbf" -"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" -"checksum js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)" = "fa5a448de267e7358beaf4a5d849518fe9a0c13fce7afd44b06e68550e5562a7" -"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -"checksum libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)" = "3baa92041a6fec78c687fa0cc2b3fae8884f743d672cf551bed1d6dac6988d0f" -"checksum linked-hash-map 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" -"checksum lock_api 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" -"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" -"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" -"checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" -"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" -"checksum memoffset 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" -"checksum mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -"checksum mime_guess 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" -"checksum miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" -"checksum mio 0.6.22 (registry+https://github.com/rust-lang/crates.io-index)" = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" -"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" -"checksum native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" -"checksum net2 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7" -"checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" -"checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" -"checksum num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" -"checksum object 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9cbca9424c482ee628fa549d9c812e2cd22f1180b9222c9200fdfa6eb31aecb2" -"checksum once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" -"checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" -"checksum openssl 0.10.29 (registry+https://github.com/rust-lang/crates.io-index)" = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd" -"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" -"checksum openssl-sys 0.9.56 (registry+https://github.com/rust-lang/crates.io-index)" = "f02309a7f127000ed50594f0b50ecc69e7c654e16d41b4e8156d1b3df8e0b52e" -"checksum oppai-rs 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4f143357550da5c04800333509df440fcbe5254120a5af05097a083caf23105b" -"checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" -"checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" -"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -"checksum pin-project 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)" = "edc93aeee735e60ecb40cf740eb319ff23eab1c5748abfdb5c180e4ce49f7791" -"checksum pin-project-internal 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)" = "e58db2081ba5b4c93bd6be09c40fd36cb9193a8336c384f3b40012e531aa7e40" -"checksum pin-project-lite 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f7505eeebd78492e0f6108f7171c4948dbb120ee8119d9d77d0afa5469bef67f" -"checksum pin-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" -"checksum ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" -"checksum proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)" = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63" -"checksum proc-macro-nested 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" -"checksum proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "de40dd4ff82d9c9bab6dae29dbab1167e515f8df9ed17d2987cb6012db206933" -"checksum quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea" -"checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -"checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -"checksum rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" -"checksum rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" -"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" -"checksum regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" -"checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" -"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" -"checksum reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)" = "02b81e49ddec5109a9dcfc5f2a317ff53377c915e9ae9d4f2fb50914b85614e2" -"checksum ring 0.16.13 (registry+https://github.com/rust-lang/crates.io-index)" = "703516ae74571f24b465b4a1431e81e2ad51336cb0ded733a55a1aa3eccac196" -"checksum rustbreak 2.0.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)" = "b1c185a2ede13fcb28feb6864ee9412a20f57bd83b4be18dc81fde4d6e786982" -"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" -"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -"checksum rustls 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" -"checksum rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1" -"checksum rustls-native-certs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a75ffeb84a6bd9d014713119542ce415db3a3e4748f0bfce1e1416cd224a23a5" -"checksum ryu 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ed3d612bc64430efeb3f7ee6ef26d590dce0c43249217bddc62112540c7941e1" -"checksum schannel 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" -"checksum scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -"checksum sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" -"checksum security-framework 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" -"checksum security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" -"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -"checksum serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)" = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c" -"checksum serde_derive 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)" = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984" -"checksum serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)" = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2" -"checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" -"checksum serde_yaml 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ef8099d3df28273c99a1728190c7a9f19d444c941044f64adf986bee7ec53051" -"checksum serenity 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3dcb21751f749cd60c0779dddb08a8d339d87b79318c536eaf875b3985c22c" -"checksum sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" -"checksum smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6" -"checksum smallvec 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" -"checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -"checksum static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -"checksum syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)" = "95b5f192649e48a5302a13f2feb224df883b98933222369e4b3b0fe2a5447269" -"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" -"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" -"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" -"checksum threadpool 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -"checksum time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" -"checksum tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d099fa27b9702bed751524694adbe393e18b36b204da91eb1cbbbbb4a5ee2d58" -"checksum tokio-rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "15cb62a0d2770787abc96e99c1cd98fcf17f94959f3af63ca85bdfb203f051b4" -"checksum tokio-tls 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" -"checksum tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" -"checksum tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" -"checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" -"checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" -"checksum tungstenite 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8a0c2bd5aeb7dcd2bb32e472c8872759308495e5eccc942e929a513cd8d36110" -"checksum typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" -"checksum typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" -"checksum unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" -"checksum unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" -"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" -"checksum unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" -"checksum untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" -"checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" -"checksum utf-8 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" -"checksum uwl 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0" -"checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" -"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" -"checksum want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -"checksum wasm-bindgen 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "e3c7d40d09cdbf0f4895ae58cf57d92e1e57a9dd8ed2e8390514b54a47cc5551" -"checksum wasm-bindgen-backend 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "c3972e137ebf830900db522d6c8fd74d1900dcfc733462e9a12e942b00b4ac94" -"checksum wasm-bindgen-futures 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "8a369c5e1dfb7569e14d62af4da642a3cbc2f9a3652fe586e26ac22222aa4b04" -"checksum wasm-bindgen-macro 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "2cd85aa2c579e8892442954685f0d801f9129de24fa2136b2c6a539c76b65776" -"checksum wasm-bindgen-macro-support 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "8eb197bd3a47553334907ffd2f16507b4f4f01bbec3ac921a7719e0decdfe72a" -"checksum wasm-bindgen-shared 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "a91c2916119c17a8e316507afaaa2dd94b47646048014bbdf6bef098c1bb58ad" -"checksum web-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)" = "8bc359e5dd3b46cb9687a051d50a2fdd228e4ba7cf6fcf861a5365c3d671a642" -"checksum webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1f50e1972865d6b1adb54167d1c8ed48606004c2c9d0ea5f1eeb34d95e863ef" -"checksum webpki-roots 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "91cd5736df7f12a964a5067a12c62fa38e1bd8080aff1f80bc29be7c80d19ab4" -"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" -"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" -"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" -"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -"checksum winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" -"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" -"checksum yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" diff --git a/youmubot-cf/Cargo.toml b/youmubot-cf/Cargo.toml index d3b8d33..0ace10a 100644 --- a/youmubot-cf/Cargo.toml +++ b/youmubot-cf/Cargo.toml @@ -7,15 +7,15 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] serde = { version = "1", features = ["derive"] } +tokio = { version = "0.2", features = ["time"] } reqwest = "0.10.1" -serenity = "0.8" +serenity = "0.9.0-rc.0" Inflector = "0.11" -codeforces = { git = "https://github.com/natsukagami/rust-codeforces-api" } +codeforces = "0.2.1" regex = "1" lazy_static = "1" -rayon = "1" chrono = { version = "0.4", features = ["serde"] } -crossbeam-channel = "0.4" +dashmap = "3.11.4" youmubot-prelude = { path = "../youmubot-prelude" } youmubot-db = { path = "../youmubot-db" } diff --git a/youmubot-cf/src/announcer.rs b/youmubot-cf/src/announcer.rs index 2cab8eb..631a75a 100644 --- a/youmubot-cf/src/announcer.rs +++ b/youmubot-cf/src/announcer.rs @@ -1,85 +1,60 @@ -use crate::db::{CfSavedUsers, CfUser}; +use crate::{ + db::{CfSavedUsers, CfUser}, + CFClient, +}; use announcer::MemberToChannels; use chrono::Utc; use codeforces::{RatingChange, User}; -use serenity::{ - framework::standard::{CommandError, CommandResult}, - http::CacheHttp, - model::id::{ChannelId, UserId}, - CacheAndHttp, -}; +use serenity::{http::CacheHttp, model::id::UserId, CacheAndHttp}; use std::sync::Arc; use youmubot_prelude::*; -type Reqwest = ::Value; - /// Updates the rating and rating changes of the users. -pub fn updates( - http: Arc, - data: AppData, - channels: MemberToChannels, -) -> CommandResult { - let mut users = CfSavedUsers::open(&*data.read()).borrow()?.clone(); - let reqwest = data.get_cloned::(); +pub struct Announcer; - for (user_id, cfu) in users.iter_mut() { - if let Err(e) = update_user(http.clone(), &channels, &reqwest, *user_id, cfu) { - dbg!((*user_id, e)); - } +#[async_trait] +impl youmubot_prelude::Announcer for Announcer { + async fn updates( + &mut self, + http: Arc, + data: AppData, + channels: MemberToChannels, + ) -> Result<()> { + let data = data.read().await; + let client = data.get::().unwrap(); + let mut users = CfSavedUsers::open(&*data).borrow()?.clone(); + + users + .iter_mut() + .map(|(user_id, cfu)| update_user(http.clone(), &channels, &client, *user_id, cfu)) + .collect::>() + .try_collect::<()>() + .await?; + *CfSavedUsers::open(&*data).borrow_mut()? = users; + Ok(()) } - - *CfSavedUsers::open(&*data.read()).borrow_mut()? = users; - Ok(()) } -fn update_user( +async fn update_user( http: Arc, channels: &MemberToChannels, - reqwest: &Reqwest, + client: &codeforces::Client, user_id: UserId, cfu: &mut CfUser, -) -> CommandResult { - // Ensure this takes 200ms - let after = crossbeam_channel::after(std::time::Duration::from_secs_f32(0.2)); - let info = User::info(reqwest, &[cfu.handle.as_str()])? +) -> Result<()> { + let info = User::info(client, &[cfu.handle.as_str()]) + .await? .into_iter() .next() - .ok_or(CommandError::from("Not found"))?; + .ok_or(Error::msg("Not found"))?; - let rating_changes = info.rating_changes(reqwest)?; + let rating_changes = info.rating_changes(client).await?; - let mut channels_list: Option> = None; + let channels_list = channels.channels_of(&http, user_id).await; cfu.last_update = Utc::now(); // Update the rating cfu.rating = info.rating; - let mut send_message = |rc: RatingChange| -> CommandResult { - let channels = - channels_list.get_or_insert_with(|| channels.channels_of(http.clone(), user_id)); - if channels.is_empty() { - return Ok(()); - } - let (contest, _, _) = - codeforces::Contest::standings(reqwest, rc.contest_id, |f| f.limit(1, 1))?; - for channel in channels { - if let Err(e) = channel.send_message(http.http(), |e| { - e.content(format!("Rating change for {}!", user_id.mention())) - .embed(|c| { - crate::embed::rating_change_embed( - &rc, - &info, - &contest, - &user_id.mention(), - c, - ) - }) - }) { - dbg!(e); - } - } - Ok(()) - }; - let rating_changes = match cfu.last_contest_id { None => rating_changes, Some(v) => { @@ -101,12 +76,46 @@ fn update_user( .or(cfu.last_contest_id); // Check for any good announcements to make - for rc in rating_changes { - if let Err(v) = send_message(rc) { - dbg!(v); - } - } - after.recv().ok(); + rating_changes + .into_iter() + .map(|rc: RatingChange| { + let channels = channels_list.clone(); + let http = http.clone(); + let info = info.clone(); + async move { + if channels.is_empty() { + return Ok(()); + } + let (contest, _, _) = + codeforces::Contest::standings(client, rc.contest_id, |f| f.limit(1, 1)) + .await?; + channels + .iter() + .map(|channel| { + channel.send_message(http.http(), |e| { + e.content(format!("Rating change for {}!", user_id.mention())) + .embed(|c| { + crate::embed::rating_change_embed( + &rc, + &info, + &contest, + &user_id.mention(), + c, + ) + }) + }) + }) + .collect::>() + .map(|v| v.map(|_| ())) + .try_collect::<()>() + .await?; + let r: Result<_> = Ok(()); + r + } + }) + .collect::>() + .try_collect::<()>() + .await?; Ok(()) } diff --git a/youmubot-cf/src/hook.rs b/youmubot-cf/src/hook.rs index 33ac2af..b69d30f 100644 --- a/youmubot-cf/src/hook.rs +++ b/youmubot-cf/src/hook.rs @@ -1,13 +1,13 @@ use chrono::{TimeZone, Utc}; -use codeforces::{Contest, Problem}; +use codeforces::{Client, Contest, Problem}; +use dashmap::DashMap as HashMap; use lazy_static::lazy_static; -use rayon::{iter::Either, prelude::*}; use regex::{Captures, Regex}; use serenity::{ builder::CreateEmbed, framework::standard::CommandError, model::channel::Message, utils::MessageBuilder, }; -use std::{collections::HashMap, sync::Arc}; +use std::{sync::Arc, time::Instant}; use youmubot_prelude::*; lazy_static! { @@ -27,106 +27,132 @@ enum ContestOrProblem { } /// Caches the contest list. -#[derive(Clone, Debug, Default)] -pub struct ContestCache(Arc>)>>>); +pub struct ContestCache { + contests: HashMap>)>, + all_list: RwLock<(Vec, Instant)>, + http: Arc, +} impl TypeMapKey for ContestCache { type Value = ContestCache; } impl ContestCache { - fn get( - &self, - http: &::Value, - contest_id: u64, - ) -> Result<(Contest, Option>), CommandError> { - let rl = self.0.read(); - match rl.get(&contest_id) { - Some(r @ (_, Some(_))) => Ok(r.clone()), - Some((c, None)) => match Contest::standings(http, contest_id, |f| f.limit(1, 1)) { - Ok((c, p, _)) => Ok({ - drop(rl); - let mut v = self.0.write(); - let v = v.entry(contest_id).or_insert((c, None)); - v.1 = Some(p); - v.clone() - }), - Err(_) => Ok((c.clone(), None)), - }, - None => { - drop(rl); - // Step 1: try to fetch it individually - match Contest::standings(http, contest_id, |f| f.limit(1, 1)) { - Ok((c, p, _)) => Ok(self - .0 - .write() - .entry(contest_id) - .or_insert((c, Some(p))) - .clone()), - Err(codeforces::Error::Codeforces(s)) if s.ends_with("has not started") => { - // Fetch the entire list - { - let mut m = self.0.write(); - let contests = Contest::list(http, contest_id > 100_000)?; - contests.into_iter().for_each(|c| { - m.entry(c.id).or_insert((c, None)); - }); - } - self.0 - .read() - .get(&contest_id) - .cloned() - .ok_or("No contest found".into()) - } - Err(e) => Err(e.into()), - } - // Step 2: try to fetch the entire list. + /// Creates a new, empty cache. + pub async fn new(http: Arc) -> Result { + let contests_list = Contest::list(&*http, true).await?; + Ok(Self { + contests: HashMap::new(), + all_list: RwLock::new((contests_list, Instant::now())), + http, + }) + } + + /// Gets a contest from the cache, fetching from upstream if possible. + pub async fn get(&self, contest_id: u64) -> Result<(Contest, Option>)> { + if let Some(v) = self.contests.get(&contest_id) { + if v.1.is_some() { + return Ok(v.clone()); } } + self.get_and_store_contest(contest_id).await + } + + async fn get_and_store_contest( + &self, + contest_id: u64, + ) -> Result<(Contest, Option>)> { + let (c, p) = match Contest::standings(&*self.http, contest_id, |f| f.limit(1, 1)).await { + Ok((c, p, _)) => (c, Some(p)), + Err(codeforces::Error::Codeforces(s)) if s.ends_with("has not started") => { + let c = self.get_from_list(contest_id).await?; + (c, None) + } + Err(v) => return Err(Error::from(v)), + }; + self.contests.insert(contest_id, (c, p)); + Ok(self.contests.get(&contest_id).unwrap().clone()) + } + + async fn get_from_list(&self, contest_id: u64) -> Result { + let last_updated = self.all_list.read().await.1.clone(); + if Instant::now() - last_updated > std::time::Duration::from_secs(60 * 60) { + // We update at most once an hour. + *self.all_list.write().await = + (Contest::list(&*self.http, true).await?, Instant::now()); + } + self.all_list + .read() + .await + .0 + .iter() + .find(|v| v.id == contest_id) + .cloned() + .ok_or_else(|| Error::msg("Contest not found")) } } /// Prints info whenever a problem or contest (or more) is sent on a channel. -pub fn codeforces_info_hook(ctx: &mut Context, m: &Message) { - if m.author.bot { - return; +pub struct InfoHook; + +#[async_trait] +impl Hook for InfoHook { + async fn call(&mut self, ctx: &Context, m: &Message) -> Result<()> { + if m.author.bot { + return Ok(()); + } + let data = ctx.data.read().await; + let contest_cache = data.get::().unwrap(); + let matches = parse(&m.content[..], contest_cache) + .collect::>() + .await; + if !matches.is_empty() { + m.channel_id + .send_message(&ctx, |c| { + c.content("Here are the info of the given Codeforces links!") + .embed(|e| print_info_message(&matches[..], e)) + }) + .await?; + } + Ok(()) } - let http = ctx.data.get_cloned::(); - let contest_cache = ctx.data.get_cloned::(); +} + +fn parse<'a>( + content: &'a str, + contest_cache: &'a ContestCache, +) -> impl stream::Stream + 'a { let matches = CONTEST_LINK - .captures_iter(&m.content) - .chain(PROBLEMSET_LINK.captures_iter(&m.content)) - // .collect::>() - // .into_par_iter() - .filter_map( - |v| match parse_capture(http.clone(), contest_cache.clone(), v) { - Ok(v) => Some(v), - Err(e) => { - dbg!(e); - None - } - }, - ) - .collect::>(); - if !matches.is_empty() { - m.channel_id - .send_message(&ctx, |c| { - c.content("Here are the info of the given Codeforces links!") - .embed(|e| print_info_message(&matches[..], e)) - }) - .ok(); - } + .captures_iter(content) + .chain(PROBLEMSET_LINK.captures_iter(content)) + .map(|v| parse_capture(contest_cache, v)) + .collect::>() + .filter_map(|v| future::ready(v.ok())); + matches } fn print_info_message<'a>( info: &[(ContestOrProblem, &str)], e: &'a mut CreateEmbed, ) -> &'a mut CreateEmbed { - let (mut problems, contests): (Vec<_>, Vec<_>) = - info.par_iter().partition_map(|(v, l)| match v { - ContestOrProblem::Problem(p) => Either::Left((p, l)), - ContestOrProblem::Contest(c, p) => Either::Right((c, p, l)), - }); + let (problems, contests): (Vec<_>, Vec<_>) = info.iter().partition(|(v, _)| match v { + ContestOrProblem::Problem(_) => true, + ContestOrProblem::Contest(_, _) => false, + }); + let mut problems = problems + .into_iter() + .map(|(v, l)| match v { + ContestOrProblem::Problem(p) => (p, l), + _ => unreachable!(), + }) + .collect::>(); + let contests = contests + .into_iter() + .map(|(v, l)| match v { + ContestOrProblem::Contest(c, p) => (c, p, l), + _ => unreachable!(), + }) + .collect::>(); problems.sort_by(|(a, _), (b, _)| a.rating.unwrap_or(1500).cmp(&b.rating.unwrap_or(1500))); let mut m = MessageBuilder::new(); if !problems.is_empty() { @@ -190,9 +216,8 @@ fn print_info_message<'a>( e.description(m.build()) } -fn parse_capture<'a>( - http: ::Value, - contest_cache: ContestCache, +async fn parse_capture<'a>( + contest_cache: &ContestCache, cap: Captures<'a>, ) -> Result<(ContestOrProblem, &'a str), CommandError> { let contest_id: u64 = cap @@ -200,7 +225,7 @@ fn parse_capture<'a>( .ok_or(CommandError::from("Contest not captured"))? .as_str() .parse()?; - let (contest, problems) = contest_cache.get(&http, contest_id)?; + let (contest, problems) = contest_cache.get(contest_id).await?; match cap.name("problem") { Some(p) => { for problem in problems.ok_or(CommandError::from("Contest hasn't started"))? { diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index 8fd487d..da182df 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -2,12 +2,12 @@ use codeforces::Contest; use serenity::{ framework::standard::{ macros::{command, group}, - Args, CommandError as Error, CommandResult, + Args, CommandResult, }, model::channel::Message, utils::MessageBuilder, }; -use std::{collections::HashMap, time::Duration}; +use std::{collections::HashMap, sync::Arc, time::Duration}; use youmubot_prelude::*; mod announcer; @@ -18,16 +18,26 @@ mod hook; /// Live-commentating a Codeforces round. mod live; +/// The TypeMapKey holding the Client. +struct CFClient; + +impl TypeMapKey for CFClient { + type Value = Arc; +} + use db::{CfSavedUsers, CfUser}; -pub use hook::codeforces_info_hook; +pub use hook::InfoHook; /// Sets up the CF databases. -pub fn setup(path: &std::path::Path, data: &mut ShareMap, announcers: &mut AnnouncerHandler) { +pub async fn setup(path: &std::path::Path, data: &mut TypeMap, announcers: &mut AnnouncerHandler) { CfSavedUsers::insert_into(data, path.join("cf_saved_users.yaml")) .expect("Must be able to set up DB"); - data.insert::(hook::ContestCache::default()); - announcers.add("codeforces", announcer::updates); + let http = data.get::().unwrap(); + let client = Arc::new(codeforces::Client::new(http.clone())); + data.insert::(hook::ContestCache::new(client.clone()).await.unwrap()); + data.insert::(client); + announcers.add("codeforces", announcer::Announcer); } #[group] @@ -43,40 +53,46 @@ pub struct Codeforces; #[usage = "[handle or tag = yourself]"] #[example = "natsukagami"] #[max_args(1)] -pub fn profile(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +pub async fn profile(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let handle = args .single::() .unwrap_or(UsernameArg::mention(m.author.id)); - let http = ctx.data.get_cloned::(); + let http = data.get::().unwrap(); let handle = match handle { UsernameArg::Raw(s) => s, UsernameArg::Tagged(u) => { - let db = CfSavedUsers::open(&*ctx.data.read()); - let db = db.borrow()?; - match db.get(&u) { - Some(v) => v.handle.clone(), + let db = CfSavedUsers::open(&*data); + let user = db.borrow()?.get(&u).map(|u| u.handle.clone()); + match user { + Some(v) => v, None => { - m.reply(&ctx, "no saved account found.")?; + m.reply(&ctx, "no saved account found.").await?; return Ok(()); } } } }; - let account = codeforces::User::info(&http, &[&handle[..]])? + let account = codeforces::User::info(&http, &[&handle[..]]) + .await? .into_iter() .next(); match account { - Some(v) => m.channel_id.send_message(&ctx, |send| { - send.content(format!( - "{}: Here is the user that you requested", - m.author.mention() - )) - .embed(|e| embed::user_embed(&v, e)) - }), - None => m.reply(&ctx, "User not found"), + Some(v) => { + m.channel_id + .send_message(&ctx, |send| { + send.content(format!( + "{}: Here is the user that you requested", + m.author.mention() + )) + .embed(|e| embed::user_embed(&v, e)) + }) + .await + } + None => m.reply(&ctx, "User not found").await, }?; Ok(()) @@ -86,28 +102,32 @@ pub fn profile(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult #[description = "Link your Codeforces account to the Discord account, to enjoy Youmu's tracking capabilities."] #[usage = "[handle]"] #[num_args(1)] -pub fn save(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +pub async fn save(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let handle = args.single::()?; - let http = ctx.data.get_cloned::(); + let http = data.get::().unwrap(); - let account = codeforces::User::info(&http, &[&handle[..]])? + let account = codeforces::User::info(&http, &[&handle[..]]) + .await? .into_iter() .next(); match account { None => { - m.reply(&ctx, "cannot find an account with such handle")?; + m.reply(&ctx, "cannot find an account with such handle") + .await?; } Some(acc) => { // Collect rating changes data. - let rating_changes = acc.rating_changes(&http)?; - let db = CfSavedUsers::open(&*ctx.data.read()); - let mut db = db.borrow_mut()?; + let rating_changes = acc.rating_changes(&http).await?; + let mut db = CfSavedUsers::open(&*data); m.reply( &ctx, format!("account `{}` has been linked to your account.", &acc.handle), - )?; - db.insert(m.author.id, CfUser::save(acc, rating_changes)); + ) + .await?; + db.borrow_mut()? + .insert(m.author.id, CfUser::save(acc, rating_changes)); } } @@ -118,9 +138,10 @@ pub fn save(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { #[description = "See the leaderboard of all people in the server."] #[only_in(guilds)] #[num_args(0)] -pub fn ranks(ctx: &mut Context, m: &Message) -> CommandResult { +pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult { + let data = ctx.data.read().await; let everyone = { - let db = CfSavedUsers::open(&*ctx.data.read()); + let db = CfSavedUsers::open(&*data); let db = db.borrow()?; db.iter() .map(|(k, v)| (k.clone(), v.clone())) @@ -129,84 +150,98 @@ pub fn ranks(ctx: &mut Context, m: &Message) -> CommandResult { let guild = m.guild_id.expect("Guild-only command"); let mut ranks = everyone .into_iter() - .filter_map(|(id, cf_user)| guild.member(&ctx, id).ok().map(|mem| (mem, cf_user))) - .collect::>(); + .map(|(id, cf_user)| { + guild + .member(&ctx, id) + .map(|mem| mem.map(|mem| (mem, cf_user))) + }) + .collect::>() + .filter_map(|v| future::ready(v.ok())) + .collect::>() + .await; ranks.sort_by(|(_, a), (_, b)| b.rating.unwrap_or(-1).cmp(&a.rating.unwrap_or(-1))); if ranks.is_empty() { - m.reply(&ctx, "No saved users in this server.")?; + m.reply(&ctx, "No saved users in this server.").await?; return Ok(()); } + let ranks = Arc::new(ranks); + const ITEMS_PER_PAGE: usize = 10; let total_pages = (ranks.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE; let last_updated = ranks.iter().map(|(_, cfu)| cfu.last_update).min().unwrap(); - ctx.data.get_cloned::().paginate_fn( - ctx.clone(), - m.channel_id, - move |page, e| { - let page = page as usize; - let start = ITEMS_PER_PAGE * page; - let end = ranks.len().min(start + ITEMS_PER_PAGE); - if start >= end { - return (e, Err(Error::from("No more pages"))); - } - let ranks = &ranks[start..end]; + paginate( + move |page, ctx, msg| { + let ranks = ranks.clone(); + Box::pin(async move { + let page = page as usize; + let start = ITEMS_PER_PAGE * page; + let end = ranks.len().min(start + ITEMS_PER_PAGE); + if start >= end { + return Ok(false); + } + let ranks = &ranks[start..end]; - let handle_width = ranks.iter().map(|(_, cfu)| cfu.handle.len()).max().unwrap(); - let username_width = ranks - .iter() - .map(|(mem, _)| mem.distinct().len()) - .max() - .unwrap(); + let handle_width = ranks.iter().map(|(_, cfu)| cfu.handle.len()).max().unwrap(); + let username_width = ranks + .iter() + .map(|(mem, _)| mem.distinct().len()) + .max() + .unwrap(); - let mut m = MessageBuilder::new(); - m.push_line("```"); + let mut m = MessageBuilder::new(); + m.push_line("```"); - // Table header - m.push_line(format!( - "Rank | Rating | {:hw$} | {:uw$}", - "Handle", - "Username", - hw = handle_width, - uw = username_width - )); - m.push_line(format!( - "----------------{:->hw$}---{:->uw$}", - "", - "", - hw = handle_width, - uw = username_width - )); - - for (id, (mem, cfu)) in ranks.iter().enumerate() { - let id = id + start + 1; + // Table header m.push_line(format!( - "{:>4} | {:>6} | {:hw$} | {:uw$}", - format!("#{}", id), - cfu.rating - .map(|v| v.to_string()) - .unwrap_or("----".to_owned()), - cfu.handle, - mem.distinct(), + "Rank | Rating | {:hw$} | {:uw$}", + "Handle", + "Username", + hw = handle_width, + uw = username_width + )); + m.push_line(format!( + "----------------{:->hw$}---{:->uw$}", + "", + "", hw = handle_width, uw = username_width )); - } - m.push_line("```"); - m.push(format!( - "Page **{}/{}**. Last updated **{}**", - page + 1, - total_pages, - last_updated.to_rfc2822() - )); + for (id, (mem, cfu)) in ranks.iter().enumerate() { + let id = id + start + 1; + m.push_line(format!( + "{:>4} | {:>6} | {:hw$} | {:uw$}", + format!("#{}", id), + cfu.rating + .map(|v| v.to_string()) + .unwrap_or("----".to_owned()), + cfu.handle, + mem.distinct(), + hw = handle_width, + uw = username_width + )); + } - (e.content(m.build()), Ok(())) + m.push_line("```"); + m.push(format!( + "Page **{}/{}**. Last updated **{}**", + page + 1, + total_pages, + last_updated.to_rfc2822() + )); + + msg.edit(ctx, |f| f.content(m.build())).await?; + Ok(true) + }) }, + ctx, + m.channel_id, std::time::Duration::from_secs(60), - )?; + ) + .await?; Ok(()) } @@ -216,23 +251,27 @@ pub fn ranks(ctx: &mut Context, m: &Message) -> CommandResult { #[usage = "[the contest id]"] #[num_args(1)] #[only_in(guilds)] -pub fn contestranks(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +pub async fn contestranks(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let contest_id: u64 = args.single()?; let guild = m.guild_id.unwrap(); // Guild-only command - let members = CfSavedUsers::open(&*ctx.data.read()).borrow()?.clone(); + let members = CfSavedUsers::open(&*data).borrow()?.clone(); let members = members .into_iter() - .filter_map(|(user_id, cf_user)| { + .map(|(user_id, cf_user)| { guild .member(&ctx, user_id) - .ok() - .map(|v| (cf_user.handle, v)) + .map(|v| v.map(|v| (cf_user.handle, v))) }) - .collect::>(); - let http = ctx.data.get_cloned::(); - let (contest, problems, ranks) = Contest::standings(&http, contest_id, |f| { + .collect::>() + .filter_map(|v| future::ready(v.ok())) + .collect::>() + .await; + let http = data.get::().unwrap(); + let (contest, problems, ranks) = Contest::standings(http, contest_id, |f| { f.handles(members.iter().map(|(k, _)| k.clone()).collect()) - })?; + }) + .await?; // Table me let ranks = ranks @@ -252,100 +291,111 @@ pub fn contestranks(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRe .collect::>(); if ranks.is_empty() { - m.reply(&ctx, "No one in this server participated in the contest...")?; + m.reply(&ctx, "No one in this server participated in the contest...") + .await?; return Ok(()); } + let ranks = Arc::new(ranks); + const ITEMS_PER_PAGE: usize = 10; let total_pages = (ranks.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE; - ctx.data.get_cloned::().paginate_fn( - ctx.clone(), - m.channel_id, - move |page, e| { - let page = page as usize; - let start = page * ITEMS_PER_PAGE; - let end = ranks.len().min(start + ITEMS_PER_PAGE); - if start >= end { - return (e, Err(Error::from("no more pages to show"))); - } - let ranks = &ranks[start..end]; - let hw = ranks - .iter() - .map(|(mem, handle, _)| format!("{} ({})", handle, mem.distinct()).len()) - .max() - .unwrap_or(0) - .max(6); - let hackw = ranks - .iter() - .map(|(_, _, row)| { - format!( - "{}/{}", - row.successful_hack_count, row.unsuccessful_hack_count - ) - .len() - }) - .max() - .unwrap_or(0) - .max(5); + paginate( + move |page, ctx, msg| { + let contest = contest.clone(); + let problems = problems.clone(); + let ranks = ranks.clone(); + Box::pin(async move { + let page = page as usize; + let start = page * ITEMS_PER_PAGE; + let end = ranks.len().min(start + ITEMS_PER_PAGE); + if start >= end { + return Ok(false); + } + let ranks = &ranks[start..end]; + let hw = ranks + .iter() + .map(|(mem, handle, _)| format!("{} ({})", handle, mem.distinct()).len()) + .max() + .unwrap_or(0) + .max(6); + let hackw = ranks + .iter() + .map(|(_, _, row)| { + format!( + "{}/{}", + row.successful_hack_count, row.unsuccessful_hack_count + ) + .len() + }) + .max() + .unwrap_or(0) + .max(5); - let mut table = MessageBuilder::new(); - let mut header = MessageBuilder::new(); - // Header - header.push(format!( - " Rank | {:hw$} | Total | {:hackw$}", - "Handle", - "Hacks", - hw = hw, - hackw = hackw - )); - for p in &problems { - header.push(format!(" | {:4}", p.index)); - } - let header = header.build(); - table - .push_line(&header) - .push_line(format!("{:-5} | {:5.0} | {: 0.0 { - table.push(format!("{:^4.0}", p.points)); - } else if let Some(_) = p.best_submission_time_seconds { - table.push(format!("{:^4}", "?")); - } else if p.rejected_attempt_count > 0 { - table.push(format!("{:^4}", format!("-{}", p.rejected_attempt_count))); - } else { - table.push(format!("{:^4}", "")); - } + for p in &problems { + header.push(format!(" | {:4}", p.index)); } - table.push_line(""); - } + let header = header.build(); + table + .push_line(&header) + .push_line(format!("{:-5} | {:5.0} | {: 0.0 { + table.push(format!("{:^4.0}", p.points)); + } else if let Some(_) = p.best_submission_time_seconds { + table.push(format!("{:^4}", "?")); + } else if p.rejected_attempt_count > 0 { + table.push(format!("{:^4}", format!("-{}", p.rejected_attempt_count))); + } else { + table.push(format!("{:^4}", "")); + } + } + table.push_line(""); + } + + let mut m = MessageBuilder::new(); + m.push_bold_safe(&contest.name) + .push(" ") + .push_line(contest.url()) + .push_codeblock(table.build(), None) + .push_line(format!("Page **{}/{}**", page + 1, total_pages)); + msg.edit(ctx, |e| e.content(m.build())).await?; + Ok(true) + }) }, + ctx, + m.channel_id, Duration::from_secs(60), ) + .await?; + Ok(()) } #[command] @@ -354,10 +404,10 @@ pub fn contestranks(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRe #[num_args(1)] #[required_permissions(MANAGE_CHANNELS)] #[only_in(guilds)] -pub fn watch(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +pub async fn watch(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { let contest_id: u64 = args.single()?; - live::watch_contest(ctx, m.guild_id.unwrap(), m.channel_id, contest_id)?; + live::watch_contest(ctx, m.guild_id.unwrap(), m.channel_id, contest_id).await?; Ok(()) } diff --git a/youmubot-cf/src/live.rs b/youmubot-cf/src/live.rs index e03146b..b2d000f 100644 --- a/youmubot-cf/src/live.rs +++ b/youmubot-cf/src/live.rs @@ -1,8 +1,6 @@ -use crate::db::CfSavedUsers; +use crate::{db::CfSavedUsers, CFClient}; use codeforces::{Contest, ContestPhase, Problem, ProblemResult, ProblemResultType, RanklistRow}; -use rayon::prelude::*; use serenity::{ - framework::standard::{CommandError, CommandResult}, model::{ guild::Member, id::{ChannelId, GuildId, UserId}, @@ -21,56 +19,64 @@ struct MemberResult { /// Watch and commentate a contest. /// /// Does the thing on a channel, block until the contest ends. -pub fn watch_contest( - ctx: &mut Context, +pub async fn watch_contest( + ctx: &Context, guild: GuildId, channel: ChannelId, contest_id: u64, -) -> CommandResult { - let db = CfSavedUsers::open(&*ctx.data.read()).borrow()?.clone(); +) -> Result<()> { + let data = ctx.data.read().await; + let db = CfSavedUsers::open(&*data).borrow()?.clone(); let http = ctx.http.clone(); // Collect an initial member list. // This never changes during the scan. let mut member_results: HashMap = db - .into_par_iter() - .filter_map(|(user_id, cfu)| { - let member = guild.member(http.clone().as_ref(), user_id).ok(); - match member { - Some(m) => Some(( - user_id, - MemberResult { - member: m, - handle: cfu.handle, - row: None, - }, - )), - None => None, + .into_iter() + .map(|(user_id, cfu)| { + let http = http.clone(); + async move { + guild.member(http, user_id).await.map(|m| { + ( + user_id, + MemberResult { + member: m, + handle: cfu.handle, + row: None, + }, + ) + }) } }) - .collect(); + .collect::>() + .filter_map(|v| future::ready(v.ok())) + .collect() + .await; - let http = ctx.data.get_cloned::(); - let (mut contest, _, _) = Contest::standings(&http, contest_id, |f| f.limit(1, 1))?; + let http = data.get::().unwrap(); + let (mut contest, _, _) = Contest::standings(&http, contest_id, |f| f.limit(1, 1)).await?; - channel.send_message(&ctx, |e| { - e.content(format!( - "Youmu is watching contest **{}**, with the following members:\n{}", - contest.name, - member_results - .iter() - .map(|(_, m)| format!("- {} as **{}**", m.member.distinct(), m.handle)) - .collect::>() - .join("\n"), - )) - })?; + channel + .send_message(&ctx, |e| { + e.content(format!( + "Youmu is watching contest **{}**, with the following members:\n{}", + contest.name, + member_results + .iter() + .map(|(_, m)| format!("- {} as **{}**", m.member.distinct(), m.handle)) + .collect::>() + .join("\n"), + )) + }) + .await?; loop { - if let Ok(messages) = scan_changes(http.clone(), &mut member_results, &mut contest) { + if let Ok(messages) = scan_changes(&*http, &mut member_results, &mut contest).await { for message in messages { channel .send_message(&ctx, |e| { e.content(format!("**{}**: {}", contest.name, message)) }) + .await .ok(); } } @@ -78,7 +84,7 @@ pub fn watch_contest( break; } // Sleep for a minute - std::thread::sleep(std::time::Duration::from_secs(60)); + tokio::time::delay_for(std::time::Duration::from_secs(60)).await; } // Announce the final results @@ -93,12 +99,14 @@ pub fn watch_contest( ranks.sort_by(|(_, a), (_, b)| a.rank.cmp(&b.rank)); if ranks.is_empty() { - channel.send_message(&ctx, |e| { - e.content(format!( - "**{}** has ended, but I can't find anyone in this server on the scoreboard...", - contest.name - )) - })?; + channel + .send_message(&ctx, |e| { + e.content(format!( + "**{}** has ended, but I can't find anyone in this server on the scoreboard...", + contest.name + )) + }) + .await?; return Ok(()); } @@ -115,23 +123,23 @@ pub fn watch_contest( row.problem_results.iter().map(|p| format!("{:.0}", p.points)).collect::>().join("/"), row.successful_hack_count, row.unsuccessful_hack_count, - )).collect::>().join("\n"))))?; + )).collect::>().join("\n")))).await?; Ok(()) } -fn scan_changes( - http: ::Value, +async fn scan_changes( + http: &codeforces::Client, members: &mut HashMap, contest: &mut Contest, -) -> Result, CommandError> { +) -> Result> { let mut messages: Vec = vec![]; let (updated_contest, problems, ranks) = { let handles = members .iter() .map(|(_, h)| h.handle.clone()) .collect::>(); - Contest::standings(&http, contest.id, |f| f.handles(handles))? + Contest::standings(&http, contest.id, |f| f.handles(handles)).await? }; // Change of phase. if contest.phase != updated_contest.phase { diff --git a/youmubot-core/Cargo.toml b/youmubot-core/Cargo.toml index 407431c..4218f4e 100644 --- a/youmubot-core/Cargo.toml +++ b/youmubot-core/Cargo.toml @@ -7,11 +7,13 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serenity = "0.8" +serenity = { version = "0.9.0-rc.0", features = ["collector"] } rand = "0.7" serde = { version = "1", features = ["derive"] } chrono = "0.4" static_assertions = "1.1" +futures-util = "0.3" +tokio = { version = "0.2", features = ["time"] } youmubot-db = { path = "../youmubot-db" } youmubot-prelude = { path = "../youmubot-prelude" } diff --git a/youmubot-core/src/admin/mod.rs b/youmubot-core/src/admin/mod.rs index 59bf137..4270883 100644 --- a/youmubot-core/src/admin/mod.rs +++ b/youmubot-core/src/admin/mod.rs @@ -1,3 +1,4 @@ +use futures_util::{stream, TryStreamExt}; use serenity::{ framework::standard::{ macros::{command, group}, @@ -9,7 +10,6 @@ use serenity::{ }, }; use soft_ban::{SOFT_BAN_COMMAND, SOFT_BAN_INIT_COMMAND}; -use std::{thread::sleep, time::Duration}; use youmubot_prelude::*; mod soft_ban; @@ -27,29 +27,34 @@ struct Admin; #[usage = "clean 50"] #[min_args(0)] #[max_args(1)] -fn clean(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { +async fn clean(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let limit = args.single().unwrap_or(10); let messages = msg .channel_id - .messages(&ctx.http, |b| b.before(msg.id).limit(limit))?; - let channel = msg.channel_id.to_channel(&ctx)?; + .messages(&ctx.http, |b| b.before(msg.id).limit(limit)) + .await?; + let channel = msg.channel_id.to_channel(&ctx).await?; match &channel { - Channel::Private(_) | Channel::Group(_) => { - let self_id = ctx.http.get_current_application_info()?.id; + Channel::Private(_) => { + let self_id = ctx.http.get_current_application_info().await?.id; messages .into_iter() .filter(|v| v.author.id == self_id) - .try_for_each(|m| m.delete(&ctx))?; + .map(|m| async move { m.delete(&ctx).await }) + .collect::>() + .try_collect::<()>() + .await?; } _ => { msg.channel_id - .delete_messages(&ctx.http, messages.into_iter())?; + .delete_messages(&ctx.http, messages.into_iter()) + .await?; } }; - msg.react(&ctx, "🌋")?; + msg.react(&ctx, '🌋').await?; if let Channel::Guild(_) = &channel { - sleep(Duration::from_secs(2)); - msg.delete(&ctx)?; + tokio::time::delay_for(std::time::Duration::from_secs(2)).await; + msg.delete(&ctx).await.ok(); } Ok(()) @@ -58,25 +63,36 @@ fn clean(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { #[command] #[required_permissions(ADMINISTRATOR)] #[description = "Ban an user with a certain reason."] -#[usage = "@user#1234/spam"] +#[usage = "tag user/[reason = none]/[days of messages to delete = 0]"] #[min_args(1)] #[max_args(2)] #[only_in("guilds")] -fn ban(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { - let user = args.single::()?.to_user(&ctx)?; - let reason = args - .remains() - .map(|v| format!("`{}`", v)) - .unwrap_or("no provided reason".to_owned()); +async fn ban(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let user = args.single::()?.to_user(&ctx).await?; + let reason = args.single::().map(|v| format!("`{}`", v)).ok(); + let dmds = args.single::().unwrap_or(0); - msg.reply( - &ctx, - format!("🔨 Banning user {} for reason `{}`.", user.tag(), reason), - )?; - - msg.guild_id - .ok_or("Can't get guild from message?")? // we had a contract - .ban(&ctx.http, user, &reason)?; + match reason { + Some(reason) => { + msg.reply( + &ctx, + format!("🔨 Banning user {} for reason `{}`.", user.tag(), reason), + ) + .await?; + msg.guild_id + .ok_or(Error::msg("Can't get guild from message?"))? // we had a contract + .ban_with_reason(&ctx.http, user, dmds, &reason) + .await?; + } + None => { + msg.reply(&ctx, format!("🔨 Banning user {}.", user.tag())) + .await?; + msg.guild_id + .ok_or(Error::msg("Can't get guild from message?"))? // we had a contract + .ban(&ctx.http, user, dmds) + .await?; + } + } Ok(()) } @@ -87,14 +103,16 @@ fn ban(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { #[usage = "@user#1234"] #[num_args(1)] #[only_in("guilds")] -fn kick(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { - let user = args.single::()?.to_user(&ctx)?; +async fn kick(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let user = args.single::()?.to_user(&ctx).await?; - msg.reply(&ctx, format!("🔫 Kicking user {}.", user.tag()))?; + msg.reply(&ctx, format!("🔫 Kicking user {}.", user.tag())) + .await?; msg.guild_id .ok_or("Can't get guild from message?")? // we had a contract - .kick(&ctx.http, user)?; + .kick(&ctx.http, user) + .await?; Ok(()) } diff --git a/youmubot-core/src/admin/soft_ban.rs b/youmubot-core/src/admin/soft_ban.rs index b8d6d25..ce2b510 100644 --- a/youmubot-core/src/admin/soft_ban.rs +++ b/youmubot-core/src/admin/soft_ban.rs @@ -1,13 +1,15 @@ use crate::db::{ServerSoftBans, SoftBans}; use chrono::offset::Utc; +use futures_util::{stream, TryStreamExt}; use serenity::{ - framework::standard::{macros::command, Args, CommandError as Error, CommandResult}, + framework::standard::{macros::command, Args, CommandResult}, model::{ channel::Message, - id::{RoleId, UserId}, + id::{GuildId, RoleId, UserId}, }, + CacheAndHttp, }; -use std::cmp::max; +use std::sync::Arc; use youmubot_prelude::*; #[command] @@ -18,57 +20,56 @@ use youmubot_prelude::*; #[min_args(1)] #[max_args(2)] #[only_in("guilds")] -pub fn soft_ban(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { - let user = args.single::()?.to_user(&ctx)?; +pub async fn soft_ban(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let user = args.single::()?.to_user(&ctx).await?; + let data = ctx.data.read().await; let duration = if args.is_empty() { None } else { - Some( - args.single::() - .map_err(|e| Error::from(&format!("{:?}", e)))?, - ) + Some(args.single::()?) }; - let guild = msg.guild_id.ok_or(Error::from("Command is guild only"))?; + let guild = msg.guild_id.ok_or(Error::msg("Command is guild only"))?; - let db = SoftBans::open(&*ctx.data.read()); - let mut db = db.borrow_mut()?; - let mut server_ban = db.get_mut(&guild).and_then(|v| match v { - ServerSoftBans::Unimplemented => None, - ServerSoftBans::Implemented(ref mut v) => Some(v), - }); - - match server_ban { + let mut db = SoftBans::open(&*data); + let val = db + .borrow()? + .get(&guild) + .map(|v| (v.role, v.periodical_bans.get(&user.id).cloned())); + let (role, current_ban_deadline) = match val { None => { - println!("get here"); - msg.reply(&ctx, format!("⚠ This server has not enabled the soft-ban feature. Check out `y!a soft-ban-init`."))?; + msg.reply(&ctx, format!("⚠ This server has not enabled the soft-ban feature. Check out `y!a soft-ban-init`.")).await?; + return Ok(()); } - Some(ref mut server_ban) => { - let mut member = guild.member(&ctx, &user)?; - match duration { - None if member.roles.contains(&server_ban.role) => { - msg.reply(&ctx, format!("⛓ Lifting soft-ban for user {}.", user.tag()))?; - member.remove_role(&ctx, server_ban.role)?; - return Ok(()); - } - None => { - msg.reply(&ctx, format!("⛓ Soft-banning user {}.", user.tag()))?; - } - Some(v) => { - let until = Utc::now() + chrono::Duration::from_std(v.0)?; - let until = server_ban - .periodical_bans - .entry(user.id) - .and_modify(|v| *v = max(*v, until)) - .or_insert(until); - msg.reply( - &ctx, - format!("⛓ Soft-banning user {} until {}.", user.tag(), until), - )?; - } - } - member.add_role(&ctx, server_ban.role)?; + Some(v) => v, + }; + + let mut member = guild.member(&ctx, &user).await?; + match duration { + None if member.roles.contains(&role) => { + msg.reply(&ctx, format!("⛓ Lifting soft-ban for user {}.", user.tag())) + .await?; + member.remove_role(&ctx, role).await?; + return Ok(()); + } + None => { + msg.reply(&ctx, format!("⛓ Soft-banning user {}.", user.tag())) + .await?; + } + Some(v) => { + // Add the duration into the ban timeout. + let until = + current_ban_deadline.unwrap_or(Utc::now()) + chrono::Duration::from_std(v.0)?; + msg.reply( + &ctx, + format!("⛓ Soft-banning user {} until {}.", user.tag(), until), + ) + .await?; + db.borrow_mut()? + .get_mut(&guild) + .map(|v| v.periodical_bans.insert(user.id, until)); } } + member.add_role(&ctx, role).await?; Ok(()) } @@ -79,86 +80,90 @@ pub fn soft_ban(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResu #[usage = "{soft_ban_role_id}"] #[num_args(1)] #[only_in("guilds")] -pub fn soft_ban_init(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { +pub async fn soft_ban_init(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let role_id = args.single::()?; - let guild = msg.guild(&ctx).ok_or(Error::from("Guild-only command"))?; - let guild = guild.read(); + let data = ctx.data.read().await; + let guild = msg.guild(&ctx).await.unwrap(); // Check whether the role_id is the one we wanted if !guild.roles.contains_key(&role_id) { - return Err(Error::from(format!( + Err(Error::msg(format!( "{} is not a role in this server.", role_id - ))); + )))?; } // Check if we already set up - let db = SoftBans::open(&*ctx.data.read()); - let mut db = db.borrow_mut()?; - let server = db - .get(&guild.id) - .map(|v| match v { - ServerSoftBans::Unimplemented => false, - _ => true, - }) - .unwrap_or(false); + let mut db = SoftBans::open(&*data); + let set_up = db.borrow()?.contains_key(&guild.id); - if !server { - db.insert(guild.id, ServerSoftBans::new_implemented(role_id)); - msg.react(&ctx, "👌")?; - Ok(()) + if !set_up { + db.borrow_mut()? + .insert(guild.id, ServerSoftBans::new(role_id)); + msg.react(&ctx, '👌').await?; } else { - Err(Error::from("Server already set up soft-bans.")) + Err(Error::msg("Server already set up soft-bans."))? + } + Ok(()) +} + +// Watch the soft bans. Blocks forever. +pub async fn watch_soft_bans(cache_http: Arc, data: AppData) { + loop { + // Scope so that locks are released + { + // Poll the data for any changes. + let db = data.read().await; + let db = SoftBans::open(&*db); + let mut db = db.borrow().unwrap().clone(); + let now = Utc::now(); + for (server_id, bans) in db.iter_mut() { + let server_name: String = match server_id.to_partial_guild(&*cache_http.http).await + { + Err(_) => continue, + Ok(v) => v.name, + }; + let to_remove: Vec<_> = bans + .periodical_bans + .iter() + .filter_map(|(user, time)| if time <= &now { Some(user) } else { None }) + .cloned() + .collect(); + if let Err(e) = to_remove + .into_iter() + .map(|user_id| { + bans.periodical_bans.remove(&user_id); + lift_soft_ban_for( + &*cache_http, + *server_id, + &server_name[..], + bans.role, + user_id, + ) + }) + .collect::>() + .try_collect::<()>() + .await + { + eprintln!("Error while scanning soft-bans list: {}", e) + } + } + } + // Sleep the thread for a minute + tokio::time::delay_for(std::time::Duration::from_secs(60)).await } } -// Watch the soft bans. -pub fn watch_soft_bans(client: &serenity::Client) -> impl FnOnce() -> () + 'static { - let cache_http = { - let cache_http = client.cache_and_http.clone(); - let cache: serenity::cache::CacheRwLock = cache_http.cache.clone().into(); - (cache, cache_http.http.clone()) - }; - let data = client.data.clone(); - return move || { - let cache_http = (&cache_http.0, &*cache_http.1); - loop { - // Scope so that locks are released - { - // Poll the data for any changes. - let db = data.read(); - let db = SoftBans::open(&*db); - let mut db = db.borrow_mut().expect("Borrowable"); - let now = Utc::now(); - for (server_id, soft_bans) in db.iter_mut() { - let server_name: String = match server_id.to_partial_guild(cache_http) { - Err(_) => continue, - Ok(v) => v.name, - }; - if let ServerSoftBans::Implemented(ref mut bans) = soft_bans { - let to_remove: Vec<_> = bans - .periodical_bans - .iter() - .filter_map(|(user, time)| if time <= &now { Some(user) } else { None }) - .cloned() - .collect(); - for user_id in to_remove { - server_id - .member(cache_http, user_id) - .and_then(|mut m| { - println!( - "Soft-ban for `{}` in server `{}` unlifted.", - m.user.read().name, - server_name - ); - m.remove_role(cache_http, bans.role) - }) - .unwrap_or(()); - bans.periodical_bans.remove(&user_id); - } - } - } - } - // Sleep the thread for a minute - std::thread::sleep(std::time::Duration::from_secs(60)) - } - }; +async fn lift_soft_ban_for( + cache_http: &CacheAndHttp, + server_id: GuildId, + server_name: &str, + ban_role: RoleId, + user_id: UserId, +) -> Result<()> { + let mut m = server_id.member(cache_http, user_id).await?; + println!( + "Soft-ban for `{}` in server `{}` unlifted.", + m.user.name, server_name + ); + m.remove_role(&cache_http.http, ban_role).await?; + Ok(()) } diff --git a/youmubot-core/src/community/mod.rs b/youmubot-core/src/community/mod.rs index 0169677..06bfc7a 100644 --- a/youmubot-core/src/community/mod.rs +++ b/youmubot-core/src/community/mod.rs @@ -9,6 +9,7 @@ use serenity::{ }, model::{ channel::{Channel, Message}, + id::RoleId, user::OnlineStatus, }, utils::MessageBuilder, @@ -30,11 +31,12 @@ struct Community; #[command] #[description = r"👑 Randomly choose an active member and mention them! Note that only online/idle users in the channel are chosen from."] -#[usage = "[title = the chosen one]"] +#[usage = "[limited roles = everyone online] / [title = the chosen one]"] #[example = "the strongest in Gensokyo"] #[bucket = "community"] -#[max_args(1)] -pub fn choose(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +#[max_args(2)] +pub async fn choose(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let role = args.find::().ok(); let title = if args.is_empty() { "the chosen one".to_owned() } else { @@ -42,29 +44,39 @@ pub fn choose(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { }; let users: Result, Error> = { - let guild = m.guild(&ctx).unwrap(); - let guild = guild.read(); + let guild = m.guild(&ctx).await.unwrap(); let presences = &guild.presences; - let channel = m.channel_id.to_channel(&ctx)?; + let channel = m.channel_id.to_channel(&ctx).await?; if let Channel::Guild(channel) = channel { - let channel = channel.read(); Ok(channel - .members(&ctx)? + .members(&ctx) + .await? .into_iter() - .filter(|v| !v.user.read().bot) - .map(|v| v.user_id()) + .filter(|v| !v.user.bot) // Filter out bots .filter(|v| { + // Filter out only online people presences - .get(v) + .get(&v.user.id) .map(|presence| { presence.status == OnlineStatus::Online || presence.status == OnlineStatus::Idle }) .unwrap_or(false) }) - .collect()) + .map(|mem| future::ready(mem)) + .collect::>() + .filter_map(|member| async move { + // Filter by role if provided + match role { + Some(role) if member.roles.iter().any(|r| role == *r) => Some(member), + None => Some(member), + _ => None, + } + }) + .collect() + .await) } else { - panic!() + unreachable!() } }; let users = users?; @@ -73,7 +85,8 @@ pub fn choose(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { m.reply( &ctx, "🍰 Have this cake for yourself because no-one is here for the gods to pick.", - )?; + ) + .await?; return Ok(()); } @@ -83,19 +96,26 @@ pub fn choose(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { &users[uniform.sample(&mut rng)] }; - m.channel_id.send_message(&ctx, |c| { - c.content( - MessageBuilder::new() - .push("👑 The Gensokyo gods have gathered around and decided, out of ") - .push_bold(format!("{}", users.len())) - .push(" potential prayers, ") - .push(winner.mention()) - .push(" will be ") - .push_bold_safe(title) - .push(". Congrats! 🎉 🎊 🥳") - .build(), - ) - })?; + m.channel_id + .send_message(&ctx, |c| { + c.content( + MessageBuilder::new() + .push("👑 The Gensokyo gods have gathered around and decided, out of ") + .push_bold(format!("{}", users.len())) + .push(" ") + .push( + role.map(|r| r.mention() + "s") + .unwrap_or("potential prayers".to_owned()), + ) + .push(", ") + .push(winner.mention()) + .push(" will be ") + .push_bold_safe(title) + .push(". Congrats! 🎉 🎊 🥳") + .build(), + ) + }) + .await?; Ok(()) } diff --git a/youmubot-core/src/community/roles.rs b/youmubot-core/src/community/roles.rs index 4d3cab0..c04fbca 100644 --- a/youmubot-core/src/community/roles.rs +++ b/youmubot-core/src/community/roles.rs @@ -1,6 +1,6 @@ use crate::db::Roles as DB; use serenity::{ - framework::standard::{macros::command, Args, CommandError as Error, CommandResult}, + framework::standard::{macros::command, Args, CommandResult}, model::{channel::Message, guild::Role, id::RoleId}, utils::MessageBuilder, }; @@ -10,18 +10,22 @@ use youmubot_prelude::*; #[description = "List all available roles in the server."] #[num_args(0)] #[only_in(guilds)] -fn list(ctx: &mut Context, m: &Message, _: Args) -> CommandResult { +async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult { let guild_id = m.guild_id.unwrap(); // only_in(guilds) + let data = ctx.data.read().await; - let db = DB::open(&*ctx.data.read()); - let db = db.borrow()?; - let roles = db.get(&guild_id).filter(|v| !v.is_empty()).cloned(); + let db = DB::open(&*data); + let roles = db + .borrow()? + .get(&guild_id) + .filter(|v| !v.is_empty()) + .cloned(); match roles { None => { - m.reply(&ctx, "No roles available for assigning.")?; + m.reply(&ctx, "No roles available for assigning.").await?; } Some(v) => { - let roles = guild_id.to_partial_guild(&ctx)?.roles; + let roles = guild_id.to_partial_guild(&ctx).await?.roles; let roles: Vec<_> = v .into_iter() .filter_map(|(_, role)| roles.get(&role.id).cloned().map(|r| (r, role.description))) @@ -29,108 +33,116 @@ fn list(ctx: &mut Context, m: &Message, _: Args) -> CommandResult { const ROLES_PER_PAGE: usize = 8; let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE; - let watcher = ctx.data.get_cloned::(); - watcher.paginate_fn( - ctx.clone(), - m.channel_id, - move |page, e| { - let page = page as usize; - let start = page * ROLES_PER_PAGE; - let end = roles.len().min(start + ROLES_PER_PAGE); - if end <= start { - return (e, Err(Error::from("No more roles to display"))); - } - let roles = &roles[start..end]; - let nw = roles // name width - .iter() - .map(|(r, _)| r.name.len()) - .max() - .unwrap() - .max(6); - let idw = roles[0].0.id.to_string().len(); - let dw = roles - .iter() - .map(|v| v.1.len()) - .max() - .unwrap() - .max(" Description ".len()); - let mut m = MessageBuilder::new(); - m.push_line("```"); + paginate( + |page, ctx, msg| { + let roles = roles.clone(); + Box::pin(async move { + let page = page as usize; + let start = page * ROLES_PER_PAGE; + let end = roles.len().min(start + ROLES_PER_PAGE); + if end <= start { + return Ok(false); + } + let roles = &roles[start..end]; + let nw = roles // name width + .iter() + .map(|(r, _)| r.name.len()) + .max() + .unwrap() + .max(6); + let idw = roles[0].0.id.to_string().len(); + let dw = roles + .iter() + .map(|v| v.1.len()) + .max() + .unwrap() + .max(" Description ".len()); + let mut m = MessageBuilder::new(); + m.push_line("```"); - // Table header - m.push_line(format!( - "{:nw$} | {:idw$} | {:dw$}", - "Name", - "ID", - "Description", - nw = nw, - idw = idw, - dw = dw, - )); - m.push_line(format!( - "{:->nw$}---{:->idw$}---{:->dw$}", - "", - "", - "", - nw = nw, - idw = idw, - dw = dw, - )); - - for (role, description) in roles.iter() { + // Table header m.push_line(format!( "{:nw$} | {:idw$} | {:dw$}", - role.name, - role.id, - description, + "Name", + "ID", + "Description", + nw = nw, + idw = idw, + dw = dw, + )); + m.push_line(format!( + "{:->nw$}---{:->idw$}---{:->dw$}", + "", + "", + "", nw = nw, idw = idw, dw = dw, )); - } - m.push_line("```"); - m.push(format!("Page **{}/{}**", page + 1, pages)); - (e.content(m.build()), Ok(())) + for (role, description) in roles.iter() { + m.push_line(format!( + "{:nw$} | {:idw$} | {:dw$}", + role.name, + role.id, + description, + nw = nw, + idw = idw, + dw = dw, + )); + } + m.push_line("```"); + m.push(format!("Page **{}/{}**", page + 1, pages)); + + msg.edit(ctx, |f| f.content(m.to_string())).await?; + Ok(true) + }) }, + ctx, + m.channel_id, std::time::Duration::from_secs(60 * 10), - )?; + ) + .await?; } }; Ok(()) } +// async fn list_pager( + #[command("role")] #[description = "Toggle a role by its name or ID."] #[example = "\"IELTS / TOEFL\""] #[num_args(1)] #[only_in(guilds)] -fn toggle(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +async fn toggle(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { let role = args.single_quoted::()?; let guild_id = m.guild_id.unwrap(); - let roles = guild_id.to_partial_guild(&ctx)?.roles; - let role = role_from_string(&role, &roles); + let guild = guild_id.to_partial_guild(&ctx).await?; + let role = role_from_string(&role, &guild.roles); match role { None => { - m.reply(&ctx, "No such role exists")?; + m.reply(&ctx, "No such role exists").await?; } Some(role) - if !DB::open(&*ctx.data.read()) + if !DB::open(&*ctx.data.read().await) .borrow()? .get(&guild_id) .map(|g| g.contains_key(&role.id)) .unwrap_or(false) => { - m.reply(&ctx, "This role is not self-assignable. Check the `listroles` command to see which role can be assigned.")?; + m.reply(&ctx, "This role is not self-assignable. Check the `listroles` command to see which role can be assigned.").await?; } Some(role) => { - let mut member = m.member(&ctx).ok_or(Error::from("Cannot find member"))?; + let mut member = guild.member(&ctx, m.author.id).await.unwrap(); if member.roles.contains(&role.id) { - member.remove_role(&ctx, &role)?; - m.reply(&ctx, format!("Role `{}` has been removed.", role.name))?; + member.remove_role(&ctx, &role).await?; + m.reply(&ctx, format!("Role `{}` has been removed.", role.name)) + .await?; } else { - member.add_role(&ctx, &role)?; - m.reply(&ctx, format!("Role `{}` has been assigned.", role.name))?; + member.add_role(&ctx, &role).await?; + m.reply(&ctx, format!("Role `{}` has been assigned.", role.name)) + .await?; } } }; @@ -144,27 +156,29 @@ fn toggle(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { #[num_args(2)] #[required_permissions(MANAGE_ROLES)] #[only_in(guilds)] -fn add(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +async fn add(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { let role = args.single_quoted::()?; + let data = ctx.data.read().await; let description = args.single::()?; let guild_id = m.guild_id.unwrap(); - let roles = guild_id.to_partial_guild(&ctx)?.roles; + let roles = guild_id.to_partial_guild(&ctx).await?.roles; let role = role_from_string(&role, &roles); match role { None => { - m.reply(&ctx, "No such role exists")?; + m.reply(&ctx, "No such role exists").await?; } Some(role) - if DB::open(&*ctx.data.read()) + if DB::open(&*data) .borrow()? .get(&guild_id) .map(|g| g.contains_key(&role.id)) .unwrap_or(false) => { - m.reply(&ctx, "This role already exists in the database.")?; + m.reply(&ctx, "This role already exists in the database.") + .await?; } Some(role) => { - DB::open(&*ctx.data.read()) + DB::open(&*data) .borrow_mut()? .entry(guild_id) .or_default() @@ -175,7 +189,7 @@ fn add(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { description, }, ); - m.react(&ctx, "👌🏼")?; + m.react(&ctx, '👌').await?; } }; Ok(()) @@ -188,31 +202,33 @@ fn add(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { #[num_args(1)] #[required_permissions(MANAGE_ROLES)] #[only_in(guilds)] -fn remove(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +async fn remove(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { let role = args.single_quoted::()?; + let data = ctx.data.read().await; let guild_id = m.guild_id.unwrap(); - let roles = guild_id.to_partial_guild(&ctx)?.roles; + let roles = guild_id.to_partial_guild(&ctx).await?.roles; let role = role_from_string(&role, &roles); match role { None => { - m.reply(&ctx, "No such role exists")?; + m.reply(&ctx, "No such role exists").await?; } Some(role) - if !DB::open(&*ctx.data.read()) + if !DB::open(&*data) .borrow()? .get(&guild_id) .map(|g| g.contains_key(&role.id)) .unwrap_or(false) => { - m.reply(&ctx, "This role does not exist in the assignable list.")?; + m.reply(&ctx, "This role does not exist in the assignable list.") + .await?; } Some(role) => { - DB::open(&*ctx.data.read()) + DB::open(&*data) .borrow_mut()? .entry(guild_id) .or_default() .remove(&role.id); - m.react(&ctx, "👌🏼")?; + m.react(&ctx, '👌').await?; } }; Ok(()) diff --git a/youmubot-core/src/community/votes.rs b/youmubot-core/src/community/votes.rs index f0460d1..30c3205 100644 --- a/youmubot-core/src/community/votes.rs +++ b/youmubot-core/src/community/votes.rs @@ -1,14 +1,18 @@ use serenity::framework::standard::CommandError as Error; use serenity::{ + collector::ReactionAction, framework::standard::{macros::command, Args, CommandResult}, model::{ - channel::{Message, Reaction, ReactionType}, + channel::{Message, ReactionType}, id::UserId, }, utils::MessageBuilder, }; -use std::collections::{HashMap as Map, HashSet as Set}; use std::time::Duration; +use std::{ + collections::{HashMap as Map, HashSet as Set}, + convert::TryFrom, +}; use youmubot_prelude::{Duration as ParseDuration, *}; #[command] @@ -19,13 +23,13 @@ use youmubot_prelude::{Duration as ParseDuration, *}; #[only_in(guilds)] #[min_args(2)] #[owner_privilege] -pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { +pub async fn vote(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { // Parse stuff first let args = args.quoted(); let _duration = args.single::()?; let duration = &_duration.0; - if *duration < Duration::from_secs(2 * 60) || *duration > Duration::from_secs(60 * 60 * 24) { - msg.reply(ctx, format!("😒 Invalid duration ({}). The voting time should be between **2 minutes** and **1 day**.", _duration))?; + if *duration < Duration::from_secs(2) || *duration > Duration::from_secs(60 * 60 * 24) { + msg.reply(ctx, format!("😒 Invalid duration ({}). The voting time should be between **2 minutes** and **1 day**.", _duration)).await?; return Ok(()); } let question = args.single::()?; @@ -41,7 +45,8 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { msg.reply( ctx, "😒 Can't have a nice voting session if you only have one choice.", - )?; + ) + .await?; return Ok(()); } if choices.len() > MAX_CHOICES { @@ -52,7 +57,8 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { "😵 Too many choices... We only support {} choices at the moment!", MAX_CHOICES ), - )?; + ) + .await?; return Ok(()); } @@ -89,123 +95,116 @@ pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { .description(MessageBuilder::new().push_bold_line_safe(&question).push("\nThis question was asked by ").push(author.mention())) .fields(fields.into_iter()) }) - })?; - msg.delete(&ctx)?; + }).await?; + msg.delete(&ctx).await?; + drop(msg); + // React on all the choices choices .iter() - .try_for_each(|(emote, _)| panel.react(&ctx, &emote[..]))?; + .map(|(emote, _)| { + panel + .react(&ctx, ReactionType::try_from(&emote[..]).unwrap()) + .map_ok(|_| ()) + }) + .collect::>() + .try_collect::<()>() + .await?; // A handler for votes. - struct VoteHandler { - pub ctx: Context, - pub msg: Message, - pub user_reactions: Map>, + let user_reactions: Map> = choices + .iter() + .map(|(emote, _)| (emote.clone(), Set::new())) + .collect(); - pub panel: Message, - } - - impl VoteHandler { - fn new(ctx: Context, msg: Message, panel: Message, choices: &[(String, String)]) -> Self { - VoteHandler { - ctx, - msg, - user_reactions: choices - .iter() - .map(|(emote, _)| (emote.clone(), Set::new())) - .collect(), - panel, - } - } - } - - impl ReactionHandler for VoteHandler { - fn handle_reaction(&mut self, reaction: &Reaction, is_add: bool) -> CommandResult { - if reaction.message_id != self.panel.id { - return Ok(()); - } - if reaction.user(&self.ctx)?.bot { - return Ok(()); - } + // Collect reactions... + let user_reactions = panel + .await_reactions(&ctx) + .removed(true) + .timeout(*duration) + .await + .fold(user_reactions, |mut set, reaction| async move { + let (reaction, is_add) = match &*reaction { + ReactionAction::Added(r) => (r, true), + ReactionAction::Removed(r) => (r, false), + }; let users = if let ReactionType::Unicode(ref s) = reaction.emoji { - if let Some(users) = self.user_reactions.get_mut(s.as_str()) { + if let Some(users) = set.get_mut(s.as_str()) { users } else { - return Ok(()); + return set; } } else { - return Ok(()); + return set; + }; + let user_id = match reaction.user_id { + Some(v) => v, + None => return set, }; if is_add { - users.insert(reaction.user_id); + users.insert(user_id); } else { - users.remove(&reaction.user_id); + users.remove(&user_id); } - Ok(()) - } + set + }) + .await; + + // Handle choices + let choice_map = choices.into_iter().collect::>(); + let mut result: Vec<(String, Vec)> = user_reactions + .into_iter() + .filter(|(_, users)| !users.is_empty()) + .map(|(emote, users)| (emote, users.into_iter().collect())) + .collect(); + + result.sort_unstable_by(|(_, v), (_, w)| w.len().cmp(&v.len())); + + if result.len() == 0 { + msg.reply( + &ctx, + MessageBuilder::new() + .push("no one answer your question ") + .push_bold_safe(&question) + .push(", sorry 😭") + .build(), + ) + .await?; + return Ok(()); } - ctx.data - .get_cloned::() - .handle_reactions_timed( - VoteHandler::new(ctx.clone(), msg.clone(), panel, &choices), - *duration, - move |vh| { - let (ctx, msg, user_reactions, panel) = - (vh.ctx, vh.msg, vh.user_reactions, vh.panel); - let choice_map = choices.into_iter().collect::>(); - let result: Vec<(String, Vec)> = user_reactions - .into_iter() - .filter(|(_, users)| !users.is_empty()) - .map(|(emote, users)| (emote, users.into_iter().collect())) - .collect(); - - if result.len() == 0 { - msg.reply( - &ctx, - MessageBuilder::new() - .push("no one answer your question ") - .push_bold_safe(&question) - .push(", sorry 😭") - .build(), - ) - .ok(); - } else { - channel - .send_message(&ctx, |c| { - c.content({ - let mut content = MessageBuilder::new(); - content - .push("@here, ") - .push(author.mention()) - .push(" previously asked ") - .push_bold_safe(&question) - .push(", and here are the results!"); - result.into_iter().for_each(|(emote, votes)| { - content - .push("\n - ") - .push_bold(format!("{}", votes.len())) - .push(" voted for ") - .push(&emote) - .push(" ") - .push_bold_safe(choice_map.get(&emote).unwrap()) - .push(": ") - .push( - votes - .into_iter() - .map(|v| v.mention()) - .collect::>() - .join(", "), - ); - }); - content.build() - }) - }) - .ok(); - } - panel.delete(&ctx).ok(); - }, - ); + channel + .send_message(&ctx, |c| { + c.content({ + let mut content = MessageBuilder::new(); + content + .push("@here, ") + .push(author.mention()) + .push(" previously asked ") + .push_bold_safe(&question) + .push(", and here are the results!"); + result.into_iter().for_each(|(emote, votes)| { + content + .push("\n - ") + .push_bold(format!("{}", votes.len())) + .push(" voted for ") + .push(&emote) + .push(" ") + .push_bold_safe(choice_map.get(&emote).unwrap()) + .push(": ") + .push( + votes + .into_iter() + .map(|v| v.mention()) + .collect::>() + .join(", "), + ); + }); + content.build() + }) + }) + .await?; + panel.delete(&ctx).await?; Ok(()) // unimplemented!(); @@ -239,3 +238,4 @@ const REACTIONS: [&'static str; 90] = [ // Assertions static_assertions::const_assert!(MAX_CHOICES <= REACTIONS.len()); +static_assertions::const_assert!(MAX_CHOICES <= REACTIONS.len()); diff --git a/youmubot-core/src/db.rs b/youmubot-core/src/db.rs index 0d601c3..02d0bc8 100644 --- a/youmubot-core/src/db.rs +++ b/youmubot-core/src/db.rs @@ -14,30 +14,25 @@ pub type Roles = DB>>; /// For the admin commands: /// - Each server might have a `soft ban` role implemented. /// - We allow periodical `soft ban` applications. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum ServerSoftBans { - Implemented(ImplementedSoftBans), - Unimplemented, -} - -impl ServerSoftBans { - // Create a new, implemented role. - pub fn new_implemented(role: RoleId) -> ServerSoftBans { - ServerSoftBans::Implemented(ImplementedSoftBans { - role, - periodical_bans: HashMap::new(), - }) - } -} #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ImplementedSoftBans { +pub struct ServerSoftBans { /// The soft-ban role. pub role: RoleId, /// List of all to-unban people. pub periodical_bans: HashMap>, } +impl ServerSoftBans { + // Create a new, implemented role. + pub fn new(role: RoleId) -> Self { + Self { + role, + periodical_bans: HashMap::new(), + } + } +} + /// Role represents an assignable role. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Role { diff --git a/youmubot-core/src/fun/images.rs b/youmubot-core/src/fun/images.rs index 8c5c32e..39f6b6f 100644 --- a/youmubot-core/src/fun/images.rs +++ b/youmubot-core/src/fun/images.rs @@ -15,24 +15,24 @@ use youmubot_prelude::*; #[description = "🖼️ Find an image with a given tag on Danbooru[nsfw]!"] #[min_args(1)] #[bucket("images")] -pub fn nsfw(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { - message_command(ctx, msg, args, Rating::Explicit) +pub async fn nsfw(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + message_command(ctx, msg, args, Rating::Explicit).await } #[command] #[description = "🖼️ Find an image with a given tag on Danbooru[safe]!"] #[min_args(1)] #[bucket("images")] -pub fn image(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { - message_command(ctx, msg, args, Rating::Safe) +pub async fn image(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + message_command(ctx, msg, args, Rating::Safe).await } #[check] #[name = "nsfw"] -fn nsfw_check(ctx: &mut Context, msg: &Message, _: &mut Args, _: &CommandOptions) -> CheckResult { - let channel = msg.channel_id.to_channel(&ctx).unwrap(); +async fn nsfw_check(ctx: &Context, msg: &Message, _: &mut Args, _: &CommandOptions) -> CheckResult { + let channel = msg.channel_id.to_channel(&ctx).await.unwrap(); if !(match channel { - Channel::Guild(guild_channel) => guild_channel.read().nsfw, + Channel::Guild(guild_channel) => guild_channel.nsfw, _ => true, }) { CheckResult::Failure(Reason::User("😣 YOU FREAKING PERVERT!!!".to_owned())) @@ -41,22 +41,31 @@ fn nsfw_check(ctx: &mut Context, msg: &Message, _: &mut Args, _: &CommandOptions } } -fn message_command(ctx: &mut Context, msg: &Message, args: Args, rating: Rating) -> CommandResult { +async fn message_command( + ctx: &Context, + msg: &Message, + args: Args, + rating: Rating, +) -> CommandResult { let tags = args.remains().unwrap_or("touhou"); - let http = ctx.data.get_cloned::(); - let image = get_image(&http, rating, tags)?; + let image = get_image( + ctx.data.read().await.get::().unwrap(), + rating, + tags, + ) + .await?; match image { - None => msg.reply(&ctx, "🖼️ No image found...\n💡 Tip: In danbooru, character names follow Japanese standards (last name before first name), so **Hakurei Reimu** might give you an image while **Reimu Hakurei** won't."), + None => msg.reply(&ctx, "🖼️ No image found...\n💡 Tip: In danbooru, character names follow Japanese standards (last name before first name), so **Hakurei Reimu** might give you an image while **Reimu Hakurei** won't.").await, Some(url) => msg.reply( &ctx, format!("🖼️ Here's the image you requested!\n\n{}", url), - ), + ).await, }?; Ok(()) } // Gets an image URL. -fn get_image( +async fn get_image( client: &::Value, rating: Rating, tags: &str, @@ -72,7 +81,7 @@ fn get_image( .query(&[("limit", "1"), ("random", "true")]) .build()?; println!("{:?}", req.url()); - let response: Vec = client.execute(req)?.json()?; + let response: Vec = client.execute(req).await?.json().await?; Ok(response .into_iter() .next() diff --git a/youmubot-core/src/fun/mod.rs b/youmubot-core/src/fun/mod.rs index 57640fc..4a27b0b 100644 --- a/youmubot-core/src/fun/mod.rs +++ b/youmubot-core/src/fun/mod.rs @@ -28,7 +28,7 @@ struct Fun; #[max_args(2)] #[usage = "[max-dice-faces = 6] / [message]"] #[example = "100 / What's my score?"] -fn roll(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { +async fn roll(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let dice = if args.is_empty() { 6 } else { @@ -36,7 +36,8 @@ fn roll(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { }; if dice == 0 { - msg.reply(&ctx, "Give me a dice with 0 faces, what do you expect 😒")?; + msg.reply(&ctx, "Give me a dice with 0 faces, what do you expect 😒") + .await?; return Ok(()); } @@ -47,24 +48,30 @@ fn roll(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { }; match args.single_quoted::() { - Ok(s) => msg.reply( - &ctx, - MessageBuilder::new() - .push("you asked ") - .push_bold_safe(s) - .push(format!( - ", so I rolled a 🎲 of **{}** faces, and got **{}**!", + Ok(s) => { + msg.reply( + &ctx, + MessageBuilder::new() + .push("you asked ") + .push_bold_safe(s) + .push(format!( + ", so I rolled a 🎲 of **{}** faces, and got **{}**!", + dice, result + )) + .build(), + ) + .await + } + Err(_) if args.is_empty() => { + msg.reply( + &ctx, + format!( + "I rolled a 🎲 of **{}** faces, and got **{}**!", dice, result - )) - .build(), - ), - Err(_) if args.is_empty() => msg.reply( - &ctx, - format!( - "I rolled a 🎲 of **{}** faces, and got **{}**!", - dice, result - ), - ), + ), + ) + .await + } Err(e) => return Err(e.into()), }?; @@ -77,7 +84,7 @@ You may prefix the first choice with `?` to make it a question! If no choices are given, Youmu defaults to `Yes!` and `No!`"#] #[usage = "[?question]/[choice #1]/[choice #2]/..."] #[example = "?What for dinner/Pizza/Hamburger"] -fn pick(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { +async fn pick(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let (question, choices) = { // Get a list of options. let mut choices = args @@ -114,24 +121,30 @@ fn pick(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { }; match question { - None => msg.reply( - &ctx, - MessageBuilder::new() - .push("Youmu picks 👉") - .push_bold_safe(choice) - .push("👈!") - .build(), - ), - Some(s) => msg.reply( - &ctx, - MessageBuilder::new() - .push("you asked ") - .push_bold_safe(s) - .push(", and Youmu picks 👉") - .push_bold_safe(choice) - .push("👈!") - .build(), - ), + None => { + msg.reply( + &ctx, + MessageBuilder::new() + .push("Youmu picks 👉") + .push_bold_safe(choice) + .push("👈!") + .build(), + ) + .await + } + Some(s) => { + msg.reply( + &ctx, + MessageBuilder::new() + .push("you asked ") + .push_bold_safe(s) + .push(", and Youmu picks 👉") + .push_bold_safe(choice) + .push("👈!") + .build(), + ) + .await + } }?; Ok(()) @@ -142,7 +155,7 @@ fn pick(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { #[usage = "[user_mention = yourself]"] #[example = "@user#1234"] #[max_args(1)] -fn name(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { +async fn name(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let user_id = if args.is_empty() { msg.author.id } else { @@ -153,15 +166,15 @@ fn name(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { "your".to_owned() } else { MessageBuilder::new() - .push_bold_safe(user_id.to_user(&ctx)?.tag()) + .push_bold_safe(user_id.to_user(&ctx).await?.tag()) .push("'s") .build() }; // Rule out a couple of cases - if user_id == ctx.http.get_current_application_info()?.id { + if user_id == ctx.http.get_current_application_info().await?.id { // This is my own user_id - msg.reply(&ctx, "😠 My name is **Youmu Konpaku**!")?; + msg.reply(&ctx, "😠 My name is **Youmu Konpaku**!").await?; return Ok(()); } @@ -173,6 +186,7 @@ fn name(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { "{} Japanese🇯🇵 name is **{} {}**!", user_mention, first_name, last_name ), - )?; + ) + .await?; Ok(()) } diff --git a/youmubot-core/src/lib.rs b/youmubot-core/src/lib.rs index 19dcbe2..c9db469 100644 --- a/youmubot-core/src/lib.rs +++ b/youmubot-core/src/lib.rs @@ -20,26 +20,30 @@ pub use fun::FUN_GROUP; pub fn setup( path: &std::path::Path, client: &serenity::client::Client, - data: &mut youmubot_prelude::ShareMap, + data: &mut TypeMap, ) -> serenity::framework::standard::CommandResult { db::SoftBans::insert_into(&mut *data, &path.join("soft_bans.yaml"))?; db::Roles::insert_into(&mut *data, &path.join("roles.yaml"))?; // Create handler threads - std::thread::spawn(admin::watch_soft_bans(client)); + tokio::spawn(admin::watch_soft_bans( + client.cache_and_http.clone(), + client.data.clone(), + )); Ok(()) } // A help command #[help] -pub fn help( - context: &mut Context, +pub async fn help( + context: &Context, msg: &Message, args: Args, help_options: &'static HelpOptions, groups: &[&'static CommandGroup], owners: HashSet, ) -> CommandResult { - help_commands::with_embeds(context, msg, args, help_options, groups, owners) + help_commands::with_embeds(context, msg, args, help_options, groups, owners).await; + Ok(()) } diff --git a/youmubot-db/Cargo.toml b/youmubot-db/Cargo.toml index 3008027..b5b70cc 100644 --- a/youmubot-db/Cargo.toml +++ b/youmubot-db/Cargo.toml @@ -7,18 +7,11 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serenity = "0.8" +serenity = "0.9.0-rc.0" dotenv = "0.15" serde = { version = "1.0", features = ["derive"] } chrono = "0.4.9" -# rand = "0.7.2" -# static_assertions = "1.1.0" -# reqwest = "0.10.1" -# regex = "1" -# lazy_static = "1" -# youmubot-osu = { path = "../youmubot-osu" } -rayon = "1.1" [dependencies.rustbreak] -version = "2.0.0-rc3" +version = "2.0.0" features = ["yaml_enc"] diff --git a/youmubot-db/src/lib.rs b/youmubot-db/src/lib.rs index af734f0..69a44f6 100644 --- a/youmubot-db/src/lib.rs +++ b/youmubot-db/src/lib.rs @@ -1,15 +1,22 @@ -use rustbreak::{deser::Yaml as Ron, FileDatabase}; +use rustbreak::{deser::Yaml, FileDatabase, RustbreakError as DBError}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serenity::{framework::standard::CommandError as Error, model::id::GuildId, prelude::*}; -use std::collections::HashMap; -use std::{cell::Cell, path::Path, sync::Arc}; +use serenity::{ + model::id::GuildId, + prelude::{TypeMap, TypeMapKey}, +}; +use std::{collections::HashMap, path::Path}; + /// GuildMap defines the guild-map type. /// It is basically a HashMap from a GuildId to a data structure. pub type GuildMap = HashMap; /// The generic DB type we will be using. pub struct DB(std::marker::PhantomData); -impl serenity::prelude::TypeMapKey for DB { - type Value = Arc>; + +/// A short type abbreviation for a FileDatabase. +type Database = FileDatabase; + +impl TypeMapKey for DB { + type Value = Database; } impl DB @@ -17,66 +24,62 @@ where for<'de> T: Deserialize<'de>, { /// Insert into a ShareMap. - pub fn insert_into(data: &mut ShareMap, path: impl AsRef) -> Result<(), Error> { - let db = FileDatabase::::from_path(path, T::default())?; - db.load().or_else(|e| { - dbg!(e); - db.save() - })?; - data.insert::>(Arc::new(db)); + pub fn insert_into(data: &mut TypeMap, path: impl AsRef) -> Result<(), DBError> { + let db = Database::::load_from_path_or_default(path)?; + data.insert::>(db); Ok(()) } /// Open a previously inserted DB. - pub fn open(data: &ShareMap) -> DBWriteGuard { - data.get::().expect("DB initialized").clone().into() + pub fn open(data: &TypeMap) -> DBWriteGuard { + data.get::().expect("DB initialized").into() } } /// The write guard for our FileDatabase. /// It wraps the FileDatabase in a write-on-drop lock. #[derive(Debug)] -pub struct DBWriteGuard +pub struct DBWriteGuard<'a, T> where T: Send + Sync + Clone + std::fmt::Debug + Serialize + DeserializeOwned, { - db: Arc>, - needs_save: Cell, + db: &'a Database, + needs_save: bool, } -impl From>> for DBWriteGuard +impl<'a, T> From<&'a Database> for DBWriteGuard<'a, T> where T: Send + Sync + Clone + std::fmt::Debug + Serialize + DeserializeOwned, { - fn from(v: Arc>) -> Self { + fn from(v: &'a Database) -> Self { DBWriteGuard { db: v, - needs_save: Cell::new(false), + needs_save: false, } } } -impl DBWriteGuard +impl<'a, T> DBWriteGuard<'a, T> where T: Send + Sync + Clone + std::fmt::Debug + Serialize + DeserializeOwned, { /// Borrows the FileDatabase. - pub fn borrow(&self) -> Result, rustbreak::RustbreakError> { - (*self).db.borrow_data() + pub fn borrow(&self) -> Result, DBError> { + self.db.borrow_data() } /// Borrows the FileDatabase for writing. - pub fn borrow_mut(&self) -> Result, rustbreak::RustbreakError> { - self.needs_save.set(true); - (*self).db.borrow_data_mut() + pub fn borrow_mut(&mut self) -> Result, DBError> { + self.needs_save = true; + self.db.borrow_data_mut() } } -impl Drop for DBWriteGuard +impl<'a, T> Drop for DBWriteGuard<'a, T> where T: Send + Sync + Clone + std::fmt::Debug + Serialize + DeserializeOwned, { fn drop(&mut self) { - if self.needs_save.get() { + if self.needs_save { if let Err(e) = self.db.save() { dbg!(e); } diff --git a/youmubot-osu/Cargo.toml b/youmubot-osu/Cargo.toml index f92a0e8..f3af18e 100644 --- a/youmubot-osu/Cargo.toml +++ b/youmubot-osu/Cargo.toml @@ -7,12 +7,11 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serenity = "0.8" +serenity = "0.9.0-rc.0" chrono = "0.4.10" reqwest = "0.10.1" serde = { version = "1.0", features = ["derive"] } bitflags = "1" -rayon = "1.1" lazy_static = "1" regex = "1" oppai-rs = "0.2.0" diff --git a/youmubot-osu/src/discord/announcer.rs b/youmubot-osu/src/discord/announcer.rs index 9264049..fed7c16 100644 --- a/youmubot-osu/src/discord/announcer.rs +++ b/youmubot-osu/src/discord/announcer.rs @@ -9,101 +9,159 @@ use crate::{ Client as Osu, }; use announcer::MemberToChannels; -use rayon::prelude::*; use serenity::{ - framework::standard::{CommandError as Error, CommandResult}, http::CacheHttp, model::id::{ChannelId, UserId}, CacheAndHttp, }; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use youmubot_prelude::*; /// osu! announcer's unique announcer key. pub const ANNOUNCER_KEY: &'static str = "osu"; -/// Announce osu! top scores. -pub fn updates(c: Arc, d: AppData, channels: MemberToChannels) -> CommandResult { - let osu = d.get_cloned::(); - let cache = d.get_cloned::(); - let oppai = d.get_cloned::(); - // For each user... - let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone(); - for (user_id, osu_user) in data.iter_mut() { - let channels = channels.channels_of(c.clone(), *user_id); - if channels.is_empty() { - continue; // We don't wanna update an user without any active server - } - osu_user.pp = match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]) - .par_iter() - .map(|m| { - handle_user_mode( - c.clone(), - &osu, - &cache, - &oppai, - &osu_user, - *user_id, - &channels[..], - *m, - d.clone(), - ) +/// The announcer struct implementing youmubot_prelude::Announcer +pub struct Announcer; + +#[async_trait] +impl youmubot_prelude::Announcer for Announcer { + async fn updates( + &mut self, + c: Arc, + d: AppData, + channels: MemberToChannels, + ) -> Result<()> { + // For each user... + let data = OsuSavedUsers::open(&*d.read().await).borrow()?.clone(); + let data = data + .into_iter() + .map(|(user_id, osu_user)| { + let d = d.clone(); + let channels = &channels; + let c = c.clone(); + async move { + let channels = channels.channels_of(c.clone(), user_id).await; + if channels.is_empty() { + return (user_id, osu_user); // We don't wanna update an user without any active server + } + let pp = match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]) + .into_iter() + .map(|m| { + handle_user_mode( + c.clone(), + &osu_user, + user_id, + channels.clone(), + *m, + d.clone(), + ) + }) + .collect::>() + .try_collect::>() + .await + { + Ok(v) => v, + Err(e) => { + eprintln!("osu: Cannot update {}: {}", osu_user.id, e); + return (user_id, osu_user); + } + }; + let last_update = chrono::Utc::now(); + ( + user_id, + OsuUser { + pp, + last_update, + ..osu_user + }, + ) + } }) - .collect::>() - { - Ok(v) => v, - Err(e) => { - eprintln!("osu: Cannot update {}: {}", osu_user.id, e.0); - continue; - } - }; - osu_user.last_update = chrono::Utc::now(); + .collect::>() + .collect::>() + .await; + // Update users + *OsuSavedUsers::open(&*d.read().await).borrow_mut()? = data; + Ok(()) } - // Update users - *OsuSavedUsers::open(&*d.read()).borrow_mut()? = data; - Ok(()) } /// Handles an user/mode scan, announces all possible new scores, return the new pp value. -fn handle_user_mode( +async fn handle_user_mode( c: Arc, - osu: &Osu, - cache: &BeatmapMetaCache, - oppai: &BeatmapCache, osu_user: &OsuUser, user_id: UserId, - channels: &[ChannelId], + channels: Vec, mode: Mode, d: AppData, ) -> Result, Error> { - let scores = scan_user(osu, osu_user, mode)?; - let user = osu - .user(UserID::ID(osu_user.id), |f| f.mode(mode))? - .ok_or(Error::from("user not found"))?; - scores - .into_par_iter() - .map(|(rank, score)| -> Result<_, Error> { - let beatmap = cache.get_beatmap_default(score.beatmap_id)?; - let content = oppai.get_beatmap(beatmap.beatmap_id)?; - Ok((rank, score, BeatmapWithMode(beatmap, mode), content)) - }) - .filter_map(|v| v.ok()) - .for_each(|(rank, score, beatmap, content)| { - for channel in (&channels).iter() { - if let Err(e) = channel.send_message(c.http(), |c| { - c.content(format!("New top record from {}!", user_id.mention())) - .embed(|e| score_embed(&score, &beatmap, &content, &user, Some(rank), e)) - }) { - dbg!(e); + let (scores, user) = { + let data = d.read().await; + let osu = data.get::().unwrap(); + let scores = scan_user(osu, osu_user, mode).await?; + let user = osu + .user(UserID::ID(osu_user.id), |f| f.mode(mode)) + .await? + .ok_or(Error::msg("user not found"))?; + (scores, user) + }; + let pp = user.pp; + spawn_future(async move { + scores + .into_iter() + .map(|(rank, score)| { + let d = d.clone(); + async move { + let data = d.read().await; + let cache = data.get::().unwrap(); + let oppai = data.get::().unwrap(); + let beatmap = cache.get_beatmap_default(score.beatmap_id).await?; + let content = oppai.get_beatmap(beatmap.beatmap_id).await?; + let r: Result<_> = Ok((rank, score, BeatmapWithMode(beatmap, mode), content)); + r } - save_beatmap(&*d.read(), *channel, &beatmap).ok(); - } - }); - Ok(user.pp) + }) + .collect::>() + .filter_map(|v| future::ready(v.ok())) + .for_each(move |(rank, score, beatmap, content)| { + let channels = channels.clone(); + let d = d.clone(); + let c = c.clone(); + let user = user.clone(); + async move { + let data = d.read().await; + for channel in (&channels).iter() { + if let Err(e) = channel + .send_message(c.http(), |c| { + c.content(format!("New top record from {}!", user_id.mention())) + .embed(|e| { + score_embed( + &score, + &beatmap, + &content, + &user, + Some(rank), + e, + ) + }) + }) + .await + { + dbg!(e); + } + save_beatmap(&*data, *channel, &beatmap).ok(); + } + } + }) + .await; + }); + Ok(pp) } -fn scan_user(osu: &Osu, u: &OsuUser, mode: Mode) -> Result, Error> { - let scores = osu.user_best(UserID::ID(u.id), |f| f.mode(mode).limit(25))?; +async fn scan_user(osu: &Osu, u: &OsuUser, mode: Mode) -> Result, Error> { + let scores = osu + .user_best(UserID::ID(u.id), |f| f.mode(mode).limit(25)) + .await?; let scores = scores .into_iter() .enumerate() diff --git a/youmubot-osu/src/discord/beatmap_cache.rs b/youmubot-osu/src/discord/beatmap_cache.rs index be3efbb..2528dd1 100644 --- a/youmubot-osu/src/discord/beatmap_cache.rs +++ b/youmubot-osu/src/discord/beatmap_cache.rs @@ -3,16 +3,14 @@ use crate::{ Client, }; use dashmap::DashMap; -use serenity::framework::standard::CommandError; use std::sync::Arc; -use youmubot_prelude::TypeMapKey; +use youmubot_prelude::*; /// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling. /// Does not cache non-Ranked beatmaps. -#[derive(Clone, Debug)] pub struct BeatmapMetaCache { - client: Client, - cache: Arc>, + client: Arc, + cache: DashMap<(u64, Mode), Beatmap>, } impl TypeMapKey for BeatmapMetaCache { @@ -21,13 +19,13 @@ impl TypeMapKey for BeatmapMetaCache { impl BeatmapMetaCache { /// Create a new beatmap cache. - pub fn new(client: Client) -> Self { + pub fn new(client: Arc) -> Self { BeatmapMetaCache { client, - cache: Arc::new(DashMap::new()), + cache: DashMap::new(), } } - fn insert_if_possible(&self, id: u64, mode: Option) -> Result { + async fn insert_if_possible(&self, id: u64, mode: Option) -> Result { let beatmap = self .client .beatmaps(crate::BeatmapRequestKind::Beatmap(id), |f| { @@ -36,35 +34,37 @@ impl BeatmapMetaCache { } f }) - .and_then(|v| { - v.into_iter() - .next() - .ok_or(CommandError::from("beatmap not found")) - })?; + .await + .and_then(|v| v.into_iter().next().ok_or(Error::msg("beatmap not found")))?; if let ApprovalStatus::Ranked(_) = beatmap.approval { self.cache.insert((id, beatmap.mode), beatmap.clone()); }; Ok(beatmap) } /// Get the given beatmap - pub fn get_beatmap(&self, id: u64, mode: Mode) -> Result { - self.cache - .get(&(id, mode)) - .map(|b| Ok(b.clone())) - .unwrap_or_else(|| self.insert_if_possible(id, Some(mode))) + pub async fn get_beatmap(&self, id: u64, mode: Mode) -> Result { + match self.cache.get(&(id, mode)).map(|v| v.clone()) { + Some(v) => Ok(v), + None => self.insert_if_possible(id, Some(mode)).await, + } } /// Get a beatmap without a mode... - pub fn get_beatmap_default(&self, id: u64) -> Result { - (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]) - .iter() - .filter_map(|&mode| { - self.cache - .get(&(id, mode)) - .filter(|b| b.mode == mode) - .map(|b| Ok(b.clone())) - }) - .next() - .unwrap_or_else(|| self.insert_if_possible(id, None)) + pub async fn get_beatmap_default(&self, id: u64) -> Result { + Ok( + match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]) + .iter() + .filter_map(|&mode| { + self.cache + .get(&(id, mode)) + .filter(|b| b.mode == mode) + .map(|b| b.clone()) + }) + .next() + { + Some(v) => v, + None => self.insert_if_possible(id, None).await?, + }, + ) } } diff --git a/youmubot-osu/src/discord/cache.rs b/youmubot-osu/src/discord/cache.rs index 9dc2d63..37bcab4 100644 --- a/youmubot-osu/src/discord/cache.rs +++ b/youmubot-osu/src/discord/cache.rs @@ -1,30 +1,26 @@ use super::db::OsuLastBeatmap; use super::BeatmapWithMode; -use serenity::{ - framework::standard::{CommandError as Error, CommandResult}, - model::id::ChannelId, - prelude::*, -}; +use serenity::model::id::ChannelId; +use youmubot_prelude::*; /// Save the beatmap into the server data storage. pub(crate) fn save_beatmap( - data: &ShareMap, + data: &TypeMap, channel_id: ChannelId, bm: &BeatmapWithMode, -) -> CommandResult { - let db = OsuLastBeatmap::open(data); - let mut db = db.borrow_mut()?; - - db.insert(channel_id, (bm.0.clone(), bm.mode())); +) -> Result<()> { + OsuLastBeatmap::open(data) + .borrow_mut()? + .insert(channel_id, (bm.0.clone(), bm.mode())); Ok(()) } /// Get the last beatmap requested from this channel. pub(crate) fn get_beatmap( - data: &ShareMap, + data: &TypeMap, channel_id: ChannelId, -) -> Result, Error> { +) -> Result> { let db = OsuLastBeatmap::open(data); let db = db.borrow()?; diff --git a/youmubot-osu/src/discord/hook.rs b/youmubot-osu/src/discord/hook.rs index 3c99f0e..a6348ad 100644 --- a/youmubot-osu/src/discord/hook.rs +++ b/youmubot-osu/src/discord/hook.rs @@ -7,12 +7,7 @@ use crate::{ }; use lazy_static::lazy_static; use regex::Regex; -use serenity::{ - builder::CreateMessage, - framework::standard::{CommandError as Error, CommandResult}, - model::channel::Message, - utils::MessageBuilder, -}; +use serenity::{builder::CreateMessage, model::channel::Message, utils::MessageBuilder}; use std::str::FromStr; use youmubot_prelude::*; @@ -26,47 +21,58 @@ lazy_static! { r"(?:https?://)?osu\.ppy\.sh/beatmapsets/(?P\d+)/?(?:\#(?Posu|taiko|fruits|mania)(?:/(?P\d+)|/?))?(?:\+(?P[A-Z]+))?" ).unwrap(); static ref SHORT_LINK_REGEX: Regex = Regex::new( - r"(?:^|\s)/b/(?P\d+)(?:/(?Posu|taiko|fruits|mania))?(?:\+(?P[A-Z]+))?" + r"(?:^|\s|\W)(?P
/b/(?P\d+)(?:/(?Posu|taiko|fruits|mania))?(?:\+(?P[A-Z]+))?)" ).unwrap(); } -pub fn hook(ctx: &mut Context, msg: &Message) -> () { - if msg.author.bot { - return; - } - let mut v = move || -> CommandResult { - let old_links = handle_old_links(ctx, &msg.content)?; - let new_links = handle_new_links(ctx, &msg.content)?; - let short_links = handle_short_links(ctx, &msg, &msg.content)?; - let mut last_beatmap = None; - for l in old_links - .into_iter() - .chain(new_links.into_iter()) - .chain(short_links.into_iter()) - { - if let Err(v) = msg.channel_id.send_message(&ctx, |m| match l.embed { - EmbedType::Beatmap(b, info, mods) => { - let t = handle_beatmap(&b, info, l.link, l.mode, mods, m); - let mode = l.mode.unwrap_or(b.mode); - last_beatmap = Some(super::BeatmapWithMode(b, mode)); - t - } - EmbedType::Beatmapset(b) => handle_beatmapset(b, l.link, l.mode, m), - }) { - println!("Error in osu! hook: {:?}", v) - } +pub fn hook<'a>( + ctx: &'a Context, + msg: &'a Message, +) -> std::pin::Pin> + Send + 'a>> { + Box::pin(async move { + if msg.author.bot { + return Ok(()); } + let (old_links, new_links, short_links) = ( + handle_old_links(ctx, &msg.content), + handle_new_links(ctx, &msg.content), + handle_short_links(ctx, &msg, &msg.content), + ); + let last_beatmap = stream::select(old_links, stream::select(new_links, short_links)) + .then(|l| async move { + let mut bm: Option = None; + msg.channel_id + .send_message(&ctx, |m| match l.embed { + EmbedType::Beatmap(b, info, mods) => { + let t = handle_beatmap(&b, info, l.link, l.mode, mods, m); + let mode = l.mode.unwrap_or(b.mode); + bm = Some(super::BeatmapWithMode(b, mode)); + t + } + EmbedType::Beatmapset(b) => handle_beatmapset(b, l.link, l.mode, m), + }) + .await?; + let r: Result<_> = Ok(bm); + r + }) + .filter_map(|v| async move { + match v { + Ok(v) => v, + Err(e) => { + eprintln!("{}", e); + None + } + } + }) + .fold(None, |_, v| async move { Some(v) }) + .await; + // Save the beatmap for query later. if let Some(t) = last_beatmap { - if let Err(v) = super::cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &t) { - dbg!(v); - } + super::cache::save_beatmap(&*ctx.data.read().await, msg.channel_id, &t)?; } Ok(()) - }; - if let Err(v) = v() { - println!("Error in osu! hook: {:?}", v) - } + }) } enum EmbedType { @@ -80,167 +86,216 @@ struct ToPrint<'a> { mode: Option, } -fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result>, Error> { - let osu = ctx.data.get_cloned::(); - let mut to_prints: Vec> = Vec::new(); - let cache = ctx.data.get_cloned::(); - for capture in OLD_LINK_REGEX.captures_iter(content) { - let req_type = capture.name("link_type").unwrap().as_str(); - let req = match req_type { - "b" => BeatmapRequestKind::Beatmap(capture["id"].parse()?), - "s" => BeatmapRequestKind::Beatmapset(capture["id"].parse()?), - _ => continue, - }; - let mode = capture - .name("mode") - .map(|v| v.as_str().parse()) - .transpose()? - .and_then(|v| { - Some(match v { - 0 => Mode::Std, - 1 => Mode::Taiko, - 2 => Mode::Catch, - 3 => Mode::Mania, - _ => return None, +fn handle_old_links<'a>( + ctx: &'a Context, + content: &'a str, +) -> impl stream::Stream> + 'a { + OLD_LINK_REGEX + .captures_iter(content) + .map(move |capture| async move { + let data = ctx.data.read().await; + let osu = data.get::().unwrap(); + let cache = data.get::().unwrap(); + let req_type = capture.name("link_type").unwrap().as_str(); + let req = match req_type { + "b" => BeatmapRequestKind::Beatmap(capture["id"].parse()?), + "s" => BeatmapRequestKind::Beatmapset(capture["id"].parse()?), + _ => unreachable!(), + }; + let mode = capture + .name("mode") + .map(|v| v.as_str().parse()) + .transpose()? + .and_then(|v| { + Some(match v { + 0 => Mode::Std, + 1 => Mode::Taiko, + 2 => Mode::Catch, + 3 => Mode::Mania, + _ => return None, + }) + }); + let beatmaps = osu + .beatmaps(req, |v| match mode { + Some(m) => v.mode(m, true), + None => v, }) - }); - let beatmaps = osu.beatmaps(req, |v| match mode { - Some(m) => v.mode(m, true), - None => v, - })?; - match req_type { - "b" => { - for b in beatmaps.into_iter() { + .await?; + if beatmaps.is_empty() { + return Ok(None); + } + let r: Result<_> = Ok(match req_type { + "b" => { + let b = beatmaps.into_iter().next().unwrap(); // collect beatmap info let mods = capture .name("mods") .map(|v| Mods::from_str(v.as_str()).ok()) .flatten() .unwrap_or(Mods::NOMOD); - let info = mode.unwrap_or(b.mode).to_oppai_mode().and_then(|mode| { - cache + let info = match mode.unwrap_or(b.mode).to_oppai_mode() { + Some(mode) => cache .get_beatmap(b.beatmap_id) + .await .and_then(|b| b.get_info_with(Some(mode), mods)) - .ok() - }); - to_prints.push(ToPrint { + .ok(), + None => None, + }; + Some(ToPrint { embed: EmbedType::Beatmap(b, info, mods), link: capture.get(0).unwrap().as_str(), mode, }) } - } - "s" => to_prints.push(ToPrint { - embed: EmbedType::Beatmapset(beatmaps), - link: capture.get(0).unwrap().as_str(), - mode, - }), - _ => (), - } - } - Ok(to_prints) + "s" => Some(ToPrint { + embed: EmbedType::Beatmapset(beatmaps), + link: capture.get(0).unwrap().as_str(), + mode, + }), + _ => None, + }); + r + }) + .collect::>() + .filter_map(|v| { + future::ready(match v { + Ok(v) => v, + Err(e) => { + eprintln!("{}", e); + None + } + }) + }) } -fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result>, Error> { - let osu = ctx.data.get_cloned::(); - let mut to_prints: Vec> = Vec::new(); - let cache = ctx.data.get_cloned::(); - for capture in NEW_LINK_REGEX.captures_iter(content) { - let mode = capture - .name("mode") - .and_then(|v| Mode::parse_from_new_site(v.as_str())); - let link = capture.get(0).unwrap().as_str(); - let req = match capture.name("beatmap_id") { - Some(ref v) => BeatmapRequestKind::Beatmap(v.as_str().parse()?), - None => { - BeatmapRequestKind::Beatmapset(capture.name("set_id").unwrap().as_str().parse()?) +fn handle_new_links<'a>( + ctx: &'a Context, + content: &'a str, +) -> impl stream::Stream> + 'a { + NEW_LINK_REGEX + .captures_iter(content) + .map(|capture| async move { + let data = ctx.data.read().await; + let osu = data.get::().unwrap(); + let cache = data.get::().unwrap(); + let mode = capture + .name("mode") + .and_then(|v| Mode::parse_from_new_site(v.as_str())); + let link = capture.get(0).unwrap().as_str(); + let req = match capture.name("beatmap_id") { + Some(ref v) => BeatmapRequestKind::Beatmap(v.as_str().parse()?), + None => BeatmapRequestKind::Beatmapset( + capture.name("set_id").unwrap().as_str().parse()?, + ), + }; + let beatmaps = osu + .beatmaps(req, |v| match mode { + Some(m) => v.mode(m, true), + None => v, + }) + .await?; + if beatmaps.is_empty() { + return Ok(None); } - }; - let beatmaps = osu.beatmaps(req, |v| match mode { - Some(m) => v.mode(m, true), - None => v, - })?; - match capture.name("beatmap_id") { - Some(_) => { - for beatmap in beatmaps.into_iter() { + let r: Result<_> = Ok(match capture.name("beatmap_id") { + Some(_) => { + let beatmap = beatmaps.into_iter().next().unwrap(); // collect beatmap info let mods = capture .name("mods") .and_then(|v| Mods::from_str(v.as_str()).ok()) .unwrap_or(Mods::NOMOD); - let info = mode - .unwrap_or(beatmap.mode) - .to_oppai_mode() - .and_then(|mode| { - cache - .get_beatmap(beatmap.beatmap_id) - .and_then(|b| b.get_info_with(Some(mode), mods)) - .ok() - }); - to_prints.push(ToPrint { + let info = match mode.unwrap_or(beatmap.mode).to_oppai_mode() { + Some(mode) => cache + .get_beatmap(beatmap.beatmap_id) + .await + .and_then(|b| b.get_info_with(Some(mode), mods)) + .ok(), + None => None, + }; + Some(ToPrint { embed: EmbedType::Beatmap(beatmap, info, mods), link, mode, }) } - } - None => to_prints.push(ToPrint { - embed: EmbedType::Beatmapset(beatmaps), - link, - mode, - }), - } - } - Ok(to_prints) + None => Some(ToPrint { + embed: EmbedType::Beatmapset(beatmaps), + link, + mode, + }), + }); + r + }) + .collect::>() + .filter_map(|v| { + future::ready(match v { + Ok(v) => v, + Err(e) => { + eprintln!("{}", e); + None + } + }) + }) } fn handle_short_links<'a>( - ctx: &mut Context, - msg: &Message, + ctx: &'a Context, + msg: &'a Message, content: &'a str, -) -> Result>, Error> { - if let Some(guild_id) = msg.guild_id { - if announcer::announcer_of(ctx, crate::discord::announcer::ANNOUNCER_KEY, guild_id)? - != Some(msg.channel_id) - { - // Disable if we are not in the server's announcer channel - return Ok(vec![]); - } - } - let osu = ctx.data.get_cloned::(); - let cache = ctx.data.get_cloned::(); - Ok(SHORT_LINK_REGEX +) -> impl stream::Stream> + 'a { + SHORT_LINK_REGEX .captures_iter(content) - .map(|capture| -> Result<_, Error> { + .map(|capture| async move { + if let Some(guild_id) = msg.guild_id { + if announcer::announcer_of(ctx, crate::discord::announcer::ANNOUNCER_KEY, guild_id) + .await? + != Some(msg.channel_id) + { + // Disable if we are not in the server's announcer channel + return Err(Error::msg("not in server announcer channel")); + } + } + let data = ctx.data.read().await; + let osu = data.get::().unwrap(); + let cache = data.get::().unwrap(); let mode = capture .name("mode") .and_then(|v| Mode::parse_from_new_site(v.as_str())); let id: u64 = capture.name("id").unwrap().as_str().parse()?; let beatmap = match mode { - Some(mode) => osu.get_beatmap(id, mode), - None => osu.get_beatmap_default(id), + Some(mode) => osu.get_beatmap(id, mode).await, + None => osu.get_beatmap_default(id).await, }?; let mods = capture .name("mods") .and_then(|v| Mods::from_str(v.as_str()).ok()) .unwrap_or(Mods::NOMOD); - let info = mode - .unwrap_or(beatmap.mode) - .to_oppai_mode() - .and_then(|mode| { - cache - .get_beatmap(beatmap.beatmap_id) - .and_then(|b| b.get_info_with(Some(mode), mods)) - .ok() - }); - Ok(ToPrint { + let info = match mode.unwrap_or(beatmap.mode).to_oppai_mode() { + Some(mode) => cache + .get_beatmap(beatmap.beatmap_id) + .await + .and_then(|b| b.get_info_with(Some(mode), mods)) + .ok(), + None => None, + }; + let r: Result<_> = Ok(ToPrint { embed: EmbedType::Beatmap(beatmap, info, mods), - link: capture.get(0).unwrap().as_str(), + link: capture.name("main").unwrap().as_str(), mode, + }); + r + }) + .collect::>() + .filter_map(|v| { + future::ready(match v { + Ok(v) => Some(v), + Err(e) => { + eprintln!("{}", e); + None + } }) }) - .filter_map(|v| v.ok()) - .collect()) } fn handle_beatmap<'a, 'b>( diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index a5b5052..bb45194 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -2,10 +2,9 @@ use crate::{ discord::beatmap_cache::BeatmapMetaCache, discord::oppai_cache::BeatmapCache, models::{Beatmap, Mode, Mods, Score, User}, - request::{BeatmapRequestKind, UserID}, + request::UserID, Client as OsuHttpClient, }; -use rayon::prelude::*; use serenity::{ framework::standard::{ macros::{command, group}, @@ -14,7 +13,7 @@ use serenity::{ model::channel::Message, utils::MessageBuilder, }; -use std::str::FromStr; +use std::{str::FromStr, sync::Arc}; use youmubot_prelude::*; mod announcer; @@ -36,7 +35,7 @@ use server_rank::{LEADERBOARD_COMMAND, SERVER_RANK_COMMAND}; pub(crate) struct OsuClient; impl TypeMapKey for OsuClient { - type Value = OsuHttpClient; + type Value = Arc; } /// Sets up the osu! command handling section. @@ -52,7 +51,7 @@ impl TypeMapKey for OsuClient { /// pub fn setup( path: &std::path::Path, - data: &mut ShareMap, + data: &mut TypeMap, announcers: &mut AnnouncerHandler, ) -> CommandResult { // Databases @@ -61,11 +60,10 @@ pub fn setup( OsuUserBests::insert_into(&mut *data, &path.join("osu_user_bests.yaml"))?; // API client - let http_client = data.get_cloned::(); - let osu_client = OsuHttpClient::new( - http_client.clone(), + let http_client = data.get::().unwrap().clone(); + let osu_client = Arc::new(OsuHttpClient::new( std::env::var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."), - ); + )); data.insert::(osu_client.clone()); data.insert::(oppai_cache::BeatmapCache::new(http_client)); data.insert::(beatmap_cache::BeatmapMetaCache::new( @@ -73,7 +71,7 @@ pub fn setup( )); // Announcer - announcers.add(announcer::ANNOUNCER_KEY, announcer::updates); + announcers.add(announcer::ANNOUNCER_KEY, announcer::Announcer); Ok(()) } @@ -101,8 +99,8 @@ struct Osu; #[description = "Receive information about an user in osu!std mode."] #[usage = "[username or user_id = your saved username]"] #[max_args(1)] -pub fn std(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { - get_user(ctx, msg, args, Mode::Std) +pub async fn std(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + get_user(ctx, msg, args, Mode::Std).await } #[command] @@ -110,8 +108,8 @@ pub fn std(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { #[description = "Receive information about an user in osu!taiko mode."] #[usage = "[username or user_id = your saved username]"] #[max_args(1)] -pub fn taiko(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { - get_user(ctx, msg, args, Mode::Taiko) +pub async fn taiko(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + get_user(ctx, msg, args, Mode::Taiko).await } #[command] @@ -119,8 +117,8 @@ pub fn taiko(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { #[description = "Receive information about an user in osu!catch mode."] #[usage = "[username or user_id = your saved username]"] #[max_args(1)] -pub fn catch(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { - get_user(ctx, msg, args, Mode::Catch) +pub async fn catch(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + get_user(ctx, msg, args, Mode::Catch).await } #[command] @@ -128,8 +126,8 @@ pub fn catch(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { #[description = "Receive information about an user in osu!mania mode."] #[usage = "[username or user_id = your saved username]"] #[max_args(1)] -pub fn mania(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { - get_user(ctx, msg, args, Mode::Mania) +pub async fn mania(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + get_user(ctx, msg, args, Mode::Mania).await } pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode); @@ -150,17 +148,15 @@ impl AsRef for BeatmapWithMode { #[description = "Save the given username as your username."] #[usage = "[username or user_id]"] #[num_args(1)] -pub fn save(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { - let osu = ctx.data.get_cloned::(); +pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; + let osu = data.get::().unwrap(); let user = args.single::()?; - let user: Option = osu.user(UserID::Auto(user), |f| f)?; + let user: Option = osu.user(UserID::Auto(user), |f| f).await?; match user { Some(u) => { - let db = OsuSavedUsers::open(&*ctx.data.read()); - let mut db = db.borrow_mut()?; - - db.insert( + OsuSavedUsers::open(&*data).borrow_mut()?.insert( msg.author.id, OsuUser { id: u.id, @@ -174,10 +170,11 @@ pub fn save(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { .push("user has been set to ") .push_mono_safe(u.username) .build(), - )?; + ) + .await?; } None => { - msg.reply(&ctx, "user not found...")?; + msg.reply(&ctx, "user not found...").await?; } } Ok(()) @@ -200,7 +197,7 @@ impl FromStr for ModeArg { fn to_user_id_query( s: Option, - data: &ShareMap, + data: &TypeMap, msg: &Message, ) -> Result { let id = match s { @@ -236,151 +233,161 @@ impl FromStr for Nth { } } -fn list_plays(plays: Vec, mode: Mode, ctx: Context, m: &Message) -> CommandResult { - let watcher = ctx.data.get_cloned::(); - let osu = ctx.data.get_cloned::(); - let beatmap_cache = ctx.data.get_cloned::(); - +async fn list_plays<'a>( + plays: Vec, + mode: Mode, + ctx: &'a Context, + m: &'a Message, +) -> CommandResult { + let plays = Arc::new(plays); if plays.is_empty() { - m.reply(&ctx, "No plays found")?; + m.reply(&ctx, "No plays found").await?; return Ok(()); } - let mut beatmaps: Vec> = vec![None; plays.len()]; - const ITEMS_PER_PAGE: usize = 5; let total_pages = (plays.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE; - watcher.paginate_fn( - ctx, - m.channel_id, - move |page, e| { - let page = page as usize; - let start = page * ITEMS_PER_PAGE; - let end = plays.len().min(start + ITEMS_PER_PAGE); - if start >= end { - return (e, Err(Error::from("No more pages"))); - } + paginate( + move |page, ctx, msg| { + let plays = plays.clone(); + Box::pin(async move { + let data = ctx.data.read().await; + let osu = data.get::().unwrap(); + let beatmap_cache = data.get::().unwrap(); + let page = page as usize; + let start = page * ITEMS_PER_PAGE; + let end = plays.len().min(start + ITEMS_PER_PAGE); + if start >= end { + return Ok(false); + } - let plays = &plays[start..end]; - let beatmaps: Vec<&mut String> = { - let b = &mut beatmaps[start..end]; - b.par_iter_mut() - .enumerate() - .map(|(i, v)| { - v.get_or_insert_with(|| { - if let Some(b) = osu.get_beatmap(plays[i].beatmap_id, mode).ok() { - let stars = beatmap_cache - .get_beatmap(b.beatmap_id) - .ok() - .and_then(|b| { - mode.to_oppai_mode().and_then(|mode| { - b.get_info_with(Some(mode), plays[i].mods).ok() - }) - }) - .map(|info| info.stars as f64) - .unwrap_or(b.difficulty.stars); - format!( - "[{:.1}*] {} - {} [{}] ({})", - stars, - b.artist, - b.title, - b.difficulty_name, - b.short_link(Some(mode), Some(plays[i].mods)), - ) - } else { - "FETCH_FAILED".to_owned() - } - }) + let hourglass = msg.react(ctx, '⌛').await?; + let plays = &plays[start..end]; + let beatmaps = plays + .iter() + .map(|play| async move { + let beatmap = osu.get_beatmap(play.beatmap_id, mode).await?; + let stars = { + let b = beatmap_cache.get_beatmap(beatmap.beatmap_id).await?; + mode.to_oppai_mode() + .and_then(|mode| b.get_info_with(Some(mode), play.mods).ok()) + .map(|info| info.stars as f64) + .unwrap_or(beatmap.difficulty.stars) + }; + let r: Result<_> = Ok(format!( + "[{:.1}*] {} - {} [{}] ({})", + stars, + beatmap.artist, + beatmap.title, + beatmap.difficulty_name, + beatmap.short_link(Some(mode), Some(play.mods)), + )); + r }) - .collect::>() - }; - let pp = plays - .iter() - .map(|p| { - p.pp.map(|pp| format!("{:.2}pp", pp)) - .or_else(|| { - beatmap_cache.get_beatmap(p.beatmap_id).ok().and_then(|b| { - mode.to_oppai_mode().and_then(|op| { - b.get_pp_from( - oppai_rs::Combo::NonFC { - max_combo: p.max_combo as u32, - misses: p.count_miss as u32, - }, - p.accuracy(mode) as f32, - Some(op), - p.mods, - ) - .ok() - .map(|pp| format!("{:.2}pp [?]", pp)) - }) - }) - }) - .unwrap_or("-".to_owned()) - }) - .collect::>(); - let pw = pp.iter().map(|v| v.len()).max().unwrap_or(2); - /*mods width*/ - let mw = plays - .iter() - .map(|v| v.mods.to_string().len()) - .max() - .unwrap() - .max(4); - /*beatmap names*/ - let bw = beatmaps.iter().map(|v| v.len()).max().unwrap().max(7); + .collect::>() + .map(|v| v.unwrap_or("FETCH_FAILED".to_owned())) + .collect::>(); + let pp = plays + .iter() + .map(|p| async move { + match p.pp.map(|pp| format!("{:.2}pp", pp)) { + Some(v) => Ok(v), + None => { + let b = beatmap_cache.get_beatmap(p.beatmap_id).await?; + let r: Result<_> = Ok(mode + .to_oppai_mode() + .and_then(|op| { + b.get_pp_from( + oppai_rs::Combo::NonFC { + max_combo: p.max_combo as u32, + misses: p.count_miss as u32, + }, + p.accuracy(mode) as f32, + Some(op), + p.mods, + ) + .ok() + .map(|pp| format!("{:.2}pp [?]", pp)) + }) + .unwrap_or("-".to_owned())); + r + } + } + }) + .collect::>() + .map(|v| v.unwrap_or("-".to_owned())) + .collect::>(); + let (beatmaps, pp) = future::join(beatmaps, pp).await; + let pw = pp.iter().map(|v| v.len()).max().unwrap_or(2); + /*mods width*/ + let mw = plays + .iter() + .map(|v| v.mods.to_string().len()) + .max() + .unwrap() + .max(4); + /*beatmap names*/ + let bw = beatmaps.iter().map(|v| v.len()).max().unwrap().max(7); - let mut m = MessageBuilder::new(); - // Table header - m.push_line(format!( - " # | {:pw$} | accuracy | rank | {:mw$} | {:bw$}", - "pp", - "mods", - "beatmap", - pw = pw, - mw = mw, - bw = bw - )); - m.push_line(format!( - "------{:-3} | {:>pw$} | {:>8} | {:^4} | {:mw$} | {:bw$}", - id + start + 1, - pp[id], - format!("{:.2}%", play.accuracy(mode)), - play.rank.to_string(), - play.mods.to_string(), - beatmap, + " # | {:pw$} | accuracy | rank | {:mw$} | {:bw$}", + "pp", + "mods", + "beatmap", pw = pw, mw = mw, bw = bw )); - } - // End - let table = m.build().replace("```", "\\`\\`\\`"); - let mut m = MessageBuilder::new(); - m.push_codeblock(table, None).push_line(format!( - "Page **{}/{}**", - page + 1, - total_pages - )); - if let None = mode.to_oppai_mode() { - m.push_line("Note: star difficulty doesn't reflect mods applied."); - } else { - m.push_line("[?] means pp was predicted by oppai-rs."); - } - (e.content(m.build()), Ok(())) + m.push_line(format!( + "------{:-3} | {:>pw$} | {:>8} | {:^4} | {:mw$} | {:bw$}", + id + start + 1, + pp[id], + format!("{:.2}%", play.accuracy(mode)), + play.rank.to_string(), + play.mods.to_string(), + beatmap, + pw = pw, + mw = mw, + bw = bw + )); + } + // End + let table = m.build().replace("```", "\\`\\`\\`"); + let mut m = MessageBuilder::new(); + m.push_codeblock(table, None).push_line(format!( + "Page **{}/{}**", + page + 1, + total_pages + )); + if let None = mode.to_oppai_mode() { + m.push_line("Note: star difficulty doesn't reflect mods applied."); + } else { + m.push_line("[?] means pp was predicted by oppai-rs."); + } + msg.edit(ctx, |f| f.content(m.to_string())).await?; + hourglass.delete(ctx).await?; + Ok(true) + }) }, + ctx, + m.channel_id, std::time::Duration::from_secs(60), ) + .await?; + Ok(()) } #[command] @@ -388,44 +395,49 @@ fn list_plays(plays: Vec, mode: Mode, ctx: Context, m: &Message) -> Comma #[usage = "#[the nth recent play = --all] / [mode (std, taiko, mania, catch) = std] / [username / user id = your saved id]"] #[example = "#1 / taiko / natsukagami"] #[max_args(3)] -pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { +pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let nth = args.single::().unwrap_or(Nth::All); let mode = args.single::().unwrap_or(ModeArg(Mode::Std)).0; - let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; + let user = to_user_id_query(args.single::().ok(), &*data, msg)?; - let osu = ctx.data.get_cloned::(); - let meta_cache = ctx.data.get_cloned::(); - let oppai = ctx.data.get_cloned::(); + let osu = data.get::().unwrap(); + let meta_cache = data.get::().unwrap(); + let oppai = data.get::().unwrap(); let user = osu - .user(user, |f| f.mode(mode))? + .user(user, |f| f.mode(mode)) + .await? .ok_or(Error::from("User not found"))?; match nth { Nth::Nth(nth) => { let recent_play = osu - .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(nth))? + .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(nth)) + .await? .into_iter() .last() .ok_or(Error::from("No such play"))?; - let beatmap = meta_cache - .get_beatmap(recent_play.beatmap_id, mode) - .unwrap(); - let content = oppai.get_beatmap(beatmap.beatmap_id)?; + let beatmap = meta_cache.get_beatmap(recent_play.beatmap_id, mode).await?; + let content = oppai.get_beatmap(beatmap.beatmap_id).await?; let beatmap_mode = BeatmapWithMode(beatmap, mode); - msg.channel_id.send_message(&ctx, |m| { - m.content(format!( - "{}: here is the play that you requested", - msg.author - )) - .embed(|m| score_embed(&recent_play, &beatmap_mode, &content, &user, None, m)) - })?; + msg.channel_id + .send_message(&ctx, |m| { + m.content(format!( + "{}: here is the play that you requested", + msg.author + )) + .embed(|m| score_embed(&recent_play, &beatmap_mode, &content, &user, None, m)) + }) + .await?; // Save the beatmap... - cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &beatmap_mode)?; + cache::save_beatmap(&*data, msg.channel_id, &beatmap_mode)?; } Nth::All => { - let plays = osu.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))?; - list_plays(plays, mode, ctx.clone(), msg)?; + let plays = osu + .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50)) + .await?; + list_plays(plays, mode, ctx, msg).await?; } } Ok(()) @@ -435,28 +447,33 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult #[description = "Show information from the last queried beatmap."] #[usage = "[mods = no mod]"] #[max_args(1)] -pub fn last(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { - let b = cache::get_beatmap(&*ctx.data.read(), msg.channel_id)?; +pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; + let b = cache::get_beatmap(&*data, msg.channel_id)?; match b { Some(BeatmapWithMode(b, m)) => { let mods = args.find::().unwrap_or(Mods::NOMOD); - let info = ctx - .data - .get_cloned::() - .get_beatmap(b.beatmap_id)? + let info = data + .get::() + .unwrap() + .get_beatmap(b.beatmap_id) + .await? .get_info_with(m.to_oppai_mode(), mods) .ok(); - msg.channel_id.send_message(&ctx, |f| { - f.content(format!( - "{}: here is the beatmap you requested!", - msg.author - )) - .embed(|c| beatmap_embed(&b, m, mods, info, c)) - })?; + msg.channel_id + .send_message(&ctx, |f| { + f.content(format!( + "{}: here is the beatmap you requested!", + msg.author + )) + .embed(|c| beatmap_embed(&b, m, mods, info, c)) + }) + .await?; } None => { - msg.reply(&ctx, "No beatmap was queried on this channel.")?; + msg.reply(&ctx, "No beatmap was queried on this channel.") + .await?; } } @@ -468,12 +485,14 @@ pub fn last(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { #[usage = "[username or tag = yourself]"] #[description = "Check your own or someone else's best record on the last beatmap. Also stores the result if possible."] #[max_args(1)] -pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { - let bm = cache::get_beatmap(&*ctx.data.read(), msg.channel_id)?; +pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; + let bm = cache::get_beatmap(&*data, msg.channel_id)?; match bm { None => { - msg.reply(&ctx, "No beatmap queried on this channel.")?; + msg.reply(&ctx, "No beatmap queried on this channel.") + .await?; } Some(bm) => { let b = &bm.0; @@ -484,31 +503,36 @@ pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult None => Some(msg.author.id), _ => None, }; - let user = to_user_id_query(username_arg, &*ctx.data.read(), msg)?; + let user = to_user_id_query(username_arg, &*data, msg)?; - let osu = ctx.data.get_cloned::(); - let oppai = ctx.data.get_cloned::(); + let osu = data.get::().unwrap(); + let oppai = data.get::().unwrap(); - let content = oppai.get_beatmap(b.beatmap_id)?; + let content = oppai.get_beatmap(b.beatmap_id).await?; let user = osu - .user(user, |f| f)? + .user(user, |f| f) + .await? .ok_or(Error::from("User not found"))?; - let scores = osu.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m))?; + let scores = osu + .scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m)) + .await?; if scores.is_empty() { - msg.reply(&ctx, "No scores found")?; + msg.reply(&ctx, "No scores found").await?; } for score in scores.iter() { - msg.channel_id.send_message(&ctx, |c| { - c.embed(|m| score_embed(score, &bm, &content, &user, None, m)) - })?; + msg.channel_id + .send_message(&ctx, |c| { + c.embed(|m| score_embed(&score, &bm, &content, &user, None, m)) + }) + .await?; } if let Some(user_id) = user_id { // Save to database - OsuUserBests::open(&*ctx.data.read()) + OsuUserBests::open(&*data) .borrow_mut()? .entry((bm.0.beatmap_id, bm.1)) .or_default() @@ -522,27 +546,32 @@ pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult #[command] #[description = "Get the n-th top record of an user."] -#[usage = "#[n-th = --all] / [mode (std, taiko, catch, mania)] = std / [username or user_id = your saved user id]"] -#[example = "#2 / taiko / natsukagami"] +#[usage = "[mode (std, taiko, catch, mania)] = std / #[n-th = --all] / [username or user_id = your saved user id]"] +#[example = "taiko / #2 / natsukagami"] #[max_args(3)] -pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { +pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let nth = args.single::().unwrap_or(Nth::All); let mode = args .single::() .map(|ModeArg(t)| t) .unwrap_or(Mode::Std); - let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; + let user = to_user_id_query(args.single::().ok(), &*data, msg)?; + let meta_cache = data.get::().unwrap(); + let osu = data.get::().unwrap(); - let osu = ctx.data.get_cloned::(); - let oppai = ctx.data.get_cloned::(); + let oppai = data.get::().unwrap(); let user = osu - .user(user, |f| f.mode(mode))? + .user(user, |f| f.mode(mode)) + .await? .ok_or(Error::from("User not found"))?; match nth { Nth::Nth(nth) => { - let top_play = osu.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(nth))?; + let top_play = osu + .user_best(UserID::ID(user.id), |f| f.mode(mode).limit(nth)) + .await?; let rank = top_play.len() as u8; @@ -550,69 +579,76 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { .into_iter() .last() .ok_or(Error::from("No such play"))?; - let beatmap = osu - .beatmaps(BeatmapRequestKind::Beatmap(top_play.beatmap_id), |f| { - f.mode(mode, true) - })? - .into_iter() - .next() - .unwrap(); - let content = oppai.get_beatmap(beatmap.beatmap_id)?; + let beatmap = meta_cache.get_beatmap(top_play.beatmap_id, mode).await?; + let content = oppai.get_beatmap(beatmap.beatmap_id).await?; let beatmap = BeatmapWithMode(beatmap, mode); - msg.channel_id.send_message(&ctx, |m| { - m.content(format!( - "{}: here is the play that you requested", - msg.author - )) - .embed(|m| score_embed(&top_play, &beatmap, &content, &user, Some(rank), m)) - })?; + msg.channel_id + .send_message(&ctx, |m| { + m.content(format!( + "{}: here is the play that you requested", + msg.author + )) + .embed(|m| score_embed(&top_play, &beatmap, &content, &user, Some(rank), m)) + }) + .await?; // Save the beatmap... - cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &beatmap)?; + cache::save_beatmap(&*data, msg.channel_id, &beatmap)?; } Nth::All => { - let plays = osu.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))?; - list_plays(plays, mode, ctx.clone(), msg)?; + let plays = osu + .user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100)) + .await?; + list_plays(plays, mode, ctx, msg).await?; } } Ok(()) } -fn get_user(ctx: &mut Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult { - let user = to_user_id_query(args.single::().ok(), &*ctx.data.read(), msg)?; - let osu = ctx.data.get_cloned::(); - let cache = ctx.data.get_cloned::(); - let user = osu.user(user, |f| f.mode(mode))?; - let oppai = ctx.data.get_cloned::(); +async fn get_user(ctx: &Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult { + let data = ctx.data.read().await; + let user = to_user_id_query(args.single::().ok(), &*data, msg)?; + let osu = data.get::().unwrap(); + let cache = data.get::().unwrap(); + let user = osu.user(user, |f| f.mode(mode)).await?; + let oppai = data.get::().unwrap(); match user { Some(u) => { - let best = osu - .user_best(UserID::ID(u.id), |f| f.limit(1).mode(mode))? + let best = match osu + .user_best(UserID::ID(u.id), |f| f.limit(1).mode(mode)) + .await? .into_iter() .next() - .map(|m| -> Result<_, Error> { - let beatmap = cache.get_beatmap(m.beatmap_id, mode)?; - let info = mode - .to_oppai_mode() - .map(|mode| -> Result<_, Error> { - Ok(oppai - .get_beatmap(m.beatmap_id)? - .get_info_with(Some(mode), m.mods)?) - }) - .transpose()?; - Ok((m, BeatmapWithMode(beatmap, mode), info)) + { + Some(m) => { + let beatmap = cache.get_beatmap(m.beatmap_id, mode).await?; + let info = match mode.to_oppai_mode() { + Some(mode) => Some( + oppai + .get_beatmap(m.beatmap_id) + .await? + .get_info_with(Some(mode), m.mods)?, + ), + None => None, + }; + Some((m, BeatmapWithMode(beatmap, mode), info)) + } + None => None, + }; + msg.channel_id + .send_message(&ctx, |m| { + m.content(format!( + "{}: here is the user that you requested", + msg.author + )) + .embed(|m| user_embed(u, best, m)) }) - .transpose()?; - msg.channel_id.send_message(&ctx, |m| { - m.content(format!( - "{}: here is the user that you requested", - msg.author - )) - .embed(|m| user_embed(u, best, m)) - }) + .await?; } - None => msg.reply(&ctx, "🔍 user not found!"), - }?; + None => { + msg.reply(&ctx, "🔍 user not found!").await?; + } + }; Ok(()) } diff --git a/youmubot-osu/src/discord/oppai_cache.rs b/youmubot-osu/src/discord/oppai_cache.rs index 60cf6b3..a446330 100644 --- a/youmubot-osu/src/discord/oppai_cache.rs +++ b/youmubot-osu/src/discord/oppai_cache.rs @@ -1,12 +1,11 @@ -use serenity::framework::standard::CommandError; use std::{ffi::CString, sync::Arc}; -use youmubot_prelude::TypeMapKey; +use youmubot_prelude::*; /// the information collected from a download/Oppai request. -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct BeatmapContent { id: u64, - content: Arc, + content: CString, } /// the output of "one" oppai run. @@ -24,7 +23,7 @@ impl BeatmapContent { accuracy: f32, mode: Option, mods: impl Into, - ) -> Result { + ) -> Result { let mut oppai = oppai_rs::Oppai::new_from_content(&self.content[..])?; oppai.combo(combo)?.accuracy(accuracy)?.mods(mods.into()); if let Some(mode) = mode { @@ -38,7 +37,7 @@ impl BeatmapContent { &self, mode: Option, mods: impl Into, - ) -> Result { + ) -> Result { let mut oppai = oppai_rs::Oppai::new_from_content(&self.content[..])?; if let Some(mode) = mode { oppai.mode(mode)?; @@ -56,39 +55,47 @@ impl BeatmapContent { } /// A central cache for the beatmaps. -#[derive(Clone, Debug)] pub struct BeatmapCache { - client: reqwest::blocking::Client, - cache: Arc>, + client: ratelimit::Ratelimit, + cache: dashmap::DashMap>, } impl BeatmapCache { /// Create a new cache. - pub fn new(client: reqwest::blocking::Client) -> Self { + pub fn new(client: reqwest::Client) -> Self { + let client = ratelimit::Ratelimit::new(client, 5, std::time::Duration::from_secs(1)); BeatmapCache { client, - cache: Arc::new(dashmap::DashMap::new()), + cache: dashmap::DashMap::new(), } } - fn download_beatmap(&self, id: u64) -> Result { + async fn download_beatmap(&self, id: u64) -> Result { let content = self .client + .borrow() + .await? .get(&format!("https://osu.ppy.sh/osu/{}", id)) - .send()? - .bytes()?; + .send() + .await? + .bytes() + .await?; Ok(BeatmapContent { id, - content: Arc::new(CString::new(content.into_iter().collect::>())?), + content: CString::new(content.into_iter().collect::>())?, }) } /// Get a beatmap from the cache. - pub fn get_beatmap(&self, id: u64) -> Result { - self.cache - .entry(id) - .or_try_insert_with(|| self.download_beatmap(id)) - .map(|v| v.clone()) + pub async fn get_beatmap( + &self, + id: u64, + ) -> Result> { + if !self.cache.contains_key(&id) { + self.cache + .insert(id, Arc::new(self.download_beatmap(id).await?)); + } + Ok(self.cache.get(&id).unwrap().clone()) } } diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index 236673d..357b78d 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -1,12 +1,14 @@ use super::{ cache::get_beatmap, db::{OsuSavedUsers, OsuUserBests}, - ModeArg, + ModeArg, OsuClient, +}; +use crate::{ + models::{Mode, Score}, + request::UserID, }; -use crate::models::{Mode, Score}; use serenity::{ - builder::EditMessage, - framework::standard::{macros::command, Args, CommandError as Error, CommandResult}, + framework::standard::{macros::command, Args, CommandResult}, model::channel::Message, utils::MessageBuilder, }; @@ -17,15 +19,15 @@ use youmubot_prelude::*; #[usage = "[mode (Std, Taiko, Catch, Mania) = Std]"] #[max_args(1)] #[only_in(guilds)] -pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let mode = args.single::().map(|v| v.0).unwrap_or(Mode::Std); let guild = m.guild_id.expect("Guild-only command"); - let users = OsuSavedUsers::open(&*ctx.data.read()) - .borrow() - .expect("DB initialized") - .iter() - .filter_map(|(user_id, osu_user)| { - guild.member(&ctx, user_id).ok().and_then(|member| { + let users = OsuSavedUsers::open(&*data).borrow()?.clone(); + let users = users + .into_iter() + .map(|(user_id, osu_user)| async move { + guild.member(&ctx, user_id).await.ok().and_then(|member| { osu_user .pp .get(mode as usize) @@ -34,7 +36,10 @@ pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRes .map(|pp| (pp, member.distinct(), osu_user.last_update.clone())) }) }) - .collect::>(); + .collect::>() + .filter_map(|v| future::ready(v)) + .collect::>() + .await; let last_update = users.iter().map(|(_, _, a)| a).min().cloned(); let mut users = users .into_iter() @@ -43,47 +48,55 @@ pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRes users.sort_by(|(a, _), (b, _)| (*b).partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); if users.is_empty() { - m.reply(&ctx, "No saved users in the current server...")?; + m.reply(&ctx, "No saved users in the current server...") + .await?; return Ok(()); } + + let users = std::sync::Arc::new(users); let last_update = last_update.unwrap(); - const ITEMS_PER_PAGE: usize = 10; - ctx.data.get_cloned::().paginate_fn( - ctx.clone(), - m.channel_id, - move |page: u8, e: &mut EditMessage| { - let start = (page as usize) * ITEMS_PER_PAGE; - let end = (start + ITEMS_PER_PAGE).min(users.len()); - if start >= end { - return (e, Err(Error("No more items".to_owned()))); - } - let total_len = users.len(); - let users = &users[start..end]; - let username_len = users.iter().map(|(_, u)| u.len()).max().unwrap().max(8); - let mut content = MessageBuilder::new(); - content - .push_line("```") - .push_line("Rank | pp | Username") - .push_line(format!("-----------------{:-= end { + return Ok(false); + } + let total_len = users.len(); + let users = &users[start..end]; + let username_len = users.iter().map(|(_, u)| u.len()).max().unwrap_or(8).max(8); + let mut content = MessageBuilder::new(); content - .push(format!( - "{:>4} | {:>7.2} | ", - format!("#{}", 1 + id + start), - pp - )) - .push_line_safe(member); - } - content.push_line("```").push_line(format!( - "Page **{}**/**{}**. Last updated: `{}`", - page + 1, - (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE, - last_update.to_rfc2822() - )); - (e.content(content.build()), Ok(())) + .push_line("```") + .push_line("Rank | pp | Username") + .push_line(format!("-----------------{:-4} | {:>7.2} | ", + format!("#{}", 1 + id + start), + pp + )) + .push_line_safe(member); + } + content.push_line("```").push_line(format!( + "Page **{}**/**{}**. Last updated: `{}`", + page + 1, + (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE, + last_update.to_rfc2822() + )); + m.edit(ctx, |f| f.content(content.to_string())).await?; + Ok(true) + }) }, + ctx, + m.channel_id, std::time::Duration::from_secs(60), - )?; + ) + .await?; Ok(()) } @@ -93,48 +106,79 @@ pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRes #[description = "See the server's ranks on the last seen beatmap"] #[max_args(0)] #[only_in(guilds)] -pub fn leaderboard(ctx: &mut Context, m: &Message, mut _args: Args) -> CommandResult { - let bm = match get_beatmap(&*ctx.data.read(), m.channel_id)? { +pub async fn leaderboard(ctx: &Context, m: &Message, mut _args: Args) -> CommandResult { + let data = ctx.data.read().await; + let mut osu_user_bests = OsuUserBests::open(&*data); + let bm = match get_beatmap(&*data, m.channel_id)? { Some(bm) => bm, None => { - m.reply(&ctx, "No beatmap queried on this channel.")?; + m.reply(&ctx, "No beatmap queried on this channel.").await?; return Ok(()); } }; + // Run a check on the user once too! + { + let osu_users = OsuSavedUsers::open(&*data); + let user = osu_users.borrow()?.get(&m.author.id).map(|v| v.id); + if let Some(id) = user { + let osu = data.get::().unwrap(); + if let Ok(scores) = osu + .scores(bm.0.beatmap_id, |f| f.user(UserID::ID(id))) + .await + { + if !scores.is_empty() { + osu_user_bests + .borrow_mut()? + .entry((bm.0.beatmap_id, bm.1)) + .or_default() + .insert(m.author.id, scores); + } + } + } + } + let guild = m.guild_id.expect("Guild-only command"); let scores = { - let users = OsuUserBests::open(&*ctx.data.read()); - let users = users.borrow()?; - let users = match users.get(&(bm.0.beatmap_id, bm.1)) { + const NO_SCORES: &'static str = + "No scores have been recorded for this beatmap. Run `osu check` to scan for yours!"; + + let users = osu_user_bests + .borrow()? + .get(&(bm.0.beatmap_id, bm.1)) + .cloned(); + let users = match users { None => { - m.reply( - &ctx, - "No scores have been recorded for this beatmap. Run `osu check` to scan for yours!", - )?; + m.reply(&ctx, NO_SCORES).await?; return Ok(()); } Some(v) if v.is_empty() => { - m.reply( - &ctx, - "No scores have been recorded for this beatmap. Run `osu check` to scan for yours!", - )?; + m.reply(&ctx, NO_SCORES).await?; return Ok(()); } Some(v) => v, }; let mut scores: Vec<(f64, String, Score)> = users - .iter() - .filter_map(|(user_id, scores)| { + .into_iter() + .map(|(user_id, scores)| async move { guild .member(&ctx, user_id) + .await .ok() .and_then(|m| Some((m.distinct(), scores))) }) - .flat_map(|(user, scores)| scores.into_iter().map(move |v| (user.clone(), v.clone()))) - .filter_map(|(user, score)| score.pp.map(|v| (v, user, score))) - .collect::>(); + .collect::>() + .filter_map(|v| future::ready(v)) + .flat_map(|(user, scores)| { + scores + .into_iter() + .map(move |v| future::ready((user.clone(), v.clone()))) + .collect::>() + }) + .filter_map(|(user, score)| future::ready(score.pp.map(|v| (v, user, score)))) + .collect::>() + .await; scores .sort_by(|(a, _, _), (b, _, _)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); scores @@ -144,115 +188,121 @@ pub fn leaderboard(ctx: &mut Context, m: &Message, mut _args: Args) -> CommandRe m.reply( &ctx, "No scores have been recorded for this beatmap. Run `osu check` to scan for yours!", - )?; + ) + .await?; return Ok(()); } - ctx.data.get_cloned::().paginate_fn( - ctx.clone(), - m.channel_id, - move |page: u8, e: &mut EditMessage| { + paginate( + move |page: u8, ctx: &Context, m: &mut Message| { const ITEMS_PER_PAGE: usize = 5; let start = (page as usize) * ITEMS_PER_PAGE; let end = (start + ITEMS_PER_PAGE).min(scores.len()); if start >= end { - return (e, Err(Error("No more items".to_owned()))); + return Box::pin(future::ready(Ok(false))); } let total_len = scores.len(); - let scores = &scores[start..end]; - // username width - let uw = scores - .iter() - .map(|(_, u, _)| u.len()) - .max() - .unwrap_or(8) - .max(8); - let accuracies = scores - .iter() - .map(|(_, _, v)| format!("{:.2}%", v.accuracy(bm.1))) - .collect::>(); - let aw = accuracies.iter().map(|v| v.len()).max().unwrap().max(3); - let misses = scores - .iter() - .map(|(_, _, v)| format!("{}", v.count_miss)) - .collect::>(); - let mw = misses.iter().map(|v| v.len()).max().unwrap().max(4); - let ranks = scores - .iter() - .map(|(_, _, v)| v.rank.to_string()) - .collect::>(); - let rw = ranks.iter().map(|v| v.len()).max().unwrap().max(4); - let pp = scores - .iter() - .map(|(pp, _, _)| format!("{:.2}", pp)) - .collect::>(); - let pw = pp.iter().map(|v| v.len()).max().unwrap_or(2); - /*mods width*/ - let mdw = scores - .iter() - .map(|(_, _, v)| v.mods.to_string().len()) - .max() - .unwrap() - .max(4); - let mut content = MessageBuilder::new(); - content - .push_line("```") - .push_line(format!( - "rank | {:>pw$} | {:mdw$} | {:rw$} | {:>aw$} | {:mw$} | {:uw$}", - "pp", - "mods", - "rank", - "acc", - "miss", - "user", - pw = pw, - mdw = mdw, - rw = rw, - aw = aw, - mw = mw, - uw = uw, - )) - .push_line(format!( - "-------{:->(); + let bm = (bm.0.clone(), bm.1.clone()); + Box::pin(async move { + // username width + let uw = scores + .iter() + .map(|(_, u, _)| u.len()) + .max() + .unwrap_or(8) + .max(8); + let accuracies = scores + .iter() + .map(|(_, _, v)| format!("{:.2}%", v.accuracy(bm.1))) + .collect::>(); + let aw = accuracies.iter().map(|v| v.len()).max().unwrap().max(3); + let misses = scores + .iter() + .map(|(_, _, v)| format!("{}", v.count_miss)) + .collect::>(); + let mw = misses.iter().map(|v| v.len()).max().unwrap().max(4); + let ranks = scores + .iter() + .map(|(_, _, v)| v.rank.to_string()) + .collect::>(); + let rw = ranks.iter().map(|v| v.len()).max().unwrap().max(4); + let pp = scores + .iter() + .map(|(pp, _, _)| format!("{:.2}", pp)) + .collect::>(); + let pw = pp.iter().map(|v| v.len()).max().unwrap_or(2); + /*mods width*/ + let mdw = scores + .iter() + .map(|(_, _, v)| v.mods.to_string().len()) + .max() + .unwrap() + .max(4); + let mut content = MessageBuilder::new(); + content + .push_line("```") + .push_line(format!( + "rank | {:>pw$} | {:mdw$} | {:rw$} | {:>aw$} | {:mw$} | {:uw$}", + "pp", + "mods", + "rank", + "acc", + "miss", + "user", + pw = pw, + mdw = mdw, + rw = rw, + aw = aw, + mw = mw, + uw = uw, + )) + .push_line(format!( + "-------{:-4} | {:>pw$} | {:>mdw$} | {:>rw$} | {:>aw$} | {:>mw$} | {:uw$}", + format!("#{}", 1 + id + start), + pp[id], + p.mods.to_string(), + ranks[id], + accuracies[id], + misses[id], + member, + pw = pw, + mdw = mdw, + rw = rw, + aw = aw, + mw = mw, + uw = uw, + )); + } + content.push_line("```").push_line(format!( + "Page **{}**/**{}**. Not seeing your scores? Run `osu check` to update.", + page + 1, + (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE, )); - for (id, (_, member, p)) in scores.iter().enumerate() { - content.push_line_safe(format!( - "{:>4} | {:>pw$} | {:>mdw$} | {:>rw$} | {:>aw$} | {:>mw$} | {:uw$}", - format!("#{}", 1 + id + start), - pp[id], - p.mods.to_string(), - ranks[id], - accuracies[id], - misses[id], - member, - pw = pw, - mdw = mdw, - rw = rw, - aw = aw, - mw = mw, - uw = uw, - )); - } - content.push_line("```").push_line(format!( - "Page **{}**/**{}**. Not seeing your scores? Run `osu check` to update.", - page + 1, - (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE, - )); - (e.content(content.build()), Ok(())) + m.edit(&ctx, |f| f.content(content.build())).await?; + Ok(true) + }) }, + ctx, + m.channel_id, std::time::Duration::from_secs(60), - )?; + ) + .await?; Ok(()) } diff --git a/youmubot-osu/src/lib.rs b/youmubot-osu/src/lib.rs index b93dbf6..b850884 100644 --- a/youmubot-osu/src/lib.rs +++ b/youmubot-osu/src/lib.rs @@ -8,16 +8,17 @@ mod test; use models::*; use request::builders::*; use request::*; -use reqwest::blocking::{Client as HTTPClient, RequestBuilder, Response}; -use serenity::framework::standard::CommandError as Error; -use std::{convert::TryInto, sync::Arc}; +use reqwest::Client as HTTPClient; +use std::convert::TryInto; +use youmubot_prelude::{ratelimit::Ratelimit, *}; + +/// The number of requests per minute to the osu! server. +const REQUESTS_PER_MINUTE: usize = 200; /// Client is the client that will perform calls to the osu! api server. -/// It's cheap to clone, so do it. -#[derive(Clone, Debug)] pub struct Client { - key: Arc, - client: HTTPClient, + client: Ratelimit, + key: String, } fn vec_try_into>(v: Vec) -> Result, T::Error> { @@ -32,50 +33,55 @@ fn vec_try_into>(v: Vec) -> Result, T:: impl Client { /// Create a new client from the given API key. - pub fn new(http_client: HTTPClient, key: String) -> Client { - Client { - key: Arc::new(key), - client: http_client, - } + pub fn new(key: String) -> Client { + let client = Ratelimit::new( + HTTPClient::new(), + REQUESTS_PER_MINUTE, + std::time::Duration::from_secs(60), + ); + Client { key, client } } - fn build_request(&self, r: RequestBuilder) -> Result { - let v = r.query(&[("k", &*self.key)]).build()?; - // dbg!(v.url()); - Ok(self.client.execute(v)?) + pub(crate) async fn build_request(&self, url: &str) -> Result { + Ok(self + .client + .borrow() + .await? + .get(url) + .query(&[("k", &*self.key)])) } - pub fn beatmaps( + pub async fn beatmaps( &self, kind: BeatmapRequestKind, f: impl FnOnce(&mut BeatmapRequestBuilder) -> &mut BeatmapRequestBuilder, - ) -> Result, Error> { + ) -> Result> { let mut r = BeatmapRequestBuilder::new(kind); f(&mut r); - let res: Vec = self.build_request(r.build(&self.client))?.json()?; + let res: Vec = r.build(&self).await?.json().await?; Ok(vec_try_into(res)?) } - pub fn user( + pub async fn user( &self, user: UserID, f: impl FnOnce(&mut UserRequestBuilder) -> &mut UserRequestBuilder, ) -> Result, Error> { let mut r = UserRequestBuilder::new(user); f(&mut r); - let res: Vec = self.build_request(r.build(&self.client))?.json()?; + let res: Vec = r.build(&self).await?.json().await?; let res = vec_try_into(res)?; Ok(res.into_iter().next()) } - pub fn scores( + pub async fn scores( &self, beatmap_id: u64, f: impl FnOnce(&mut ScoreRequestBuilder) -> &mut ScoreRequestBuilder, ) -> Result, Error> { let mut r = ScoreRequestBuilder::new(beatmap_id); f(&mut r); - let res: Vec = self.build_request(r.build(&self.client))?.json()?; + let res: Vec = r.build(&self).await?.json().await?; let mut res: Vec = vec_try_into(res)?; // with a scores request you need to fill the beatmap ids yourself @@ -85,23 +91,23 @@ impl Client { Ok(res) } - pub fn user_best( + pub async fn user_best( &self, user: UserID, f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder, ) -> Result, Error> { - self.user_scores(UserScoreType::Best, user, f) + self.user_scores(UserScoreType::Best, user, f).await } - pub fn user_recent( + pub async fn user_recent( &self, user: UserID, f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder, ) -> Result, Error> { - self.user_scores(UserScoreType::Recent, user, f) + self.user_scores(UserScoreType::Recent, user, f).await } - fn user_scores( + async fn user_scores( &self, u: UserScoreType, user: UserID, @@ -109,7 +115,7 @@ impl Client { ) -> Result, Error> { let mut r = UserScoreRequestBuilder::new(u, user); f(&mut r); - let res: Vec = self.build_request(r.build(&self.client))?.json()?; + let res: Vec = r.build(&self).await?.json().await?; let res = vec_try_into(res)?; Ok(res) } diff --git a/youmubot-osu/src/request.rs b/youmubot-osu/src/request.rs index 8f342cb..5f1004c 100644 --- a/youmubot-osu/src/request.rs +++ b/youmubot-osu/src/request.rs @@ -1,6 +1,7 @@ use crate::models::{Mode, Mods}; +use crate::Client; use chrono::{DateTime, Utc}; -use reqwest::blocking::{Client, RequestBuilder}; +use youmubot_prelude::*; trait ToQuery { fn to_query(&self) -> Vec<(&'static str, String)>; @@ -84,6 +85,8 @@ impl ToQuery for BeatmapRequestKind { } pub mod builders { + use reqwest::Response; + use super::*; /// A builder for a Beatmap request. pub struct BeatmapRequestBuilder { @@ -110,12 +113,15 @@ pub mod builders { self } - pub(crate) fn build(self, client: &Client) -> RequestBuilder { - client - .get("https://osu.ppy.sh/api/get_beatmaps") + pub(crate) async fn build(self, client: &Client) -> Result { + Ok(client + .build_request("https://osu.ppy.sh/api/get_beatmaps") + .await? .query(&self.kind.to_query()) .query(&self.since.map(|v| ("since", v)).to_query()) .query(&self.mode.to_query()) + .send() + .await?) } } @@ -144,9 +150,10 @@ pub mod builders { self } - pub(crate) fn build(&self, client: &Client) -> RequestBuilder { - client - .get("https://osu.ppy.sh/api/get_user") + pub(crate) async fn build(&self, client: &Client) -> Result { + Ok(client + .build_request("https://osu.ppy.sh/api/get_user") + .await? .query(&self.user.to_query()) .query(&self.mode.to_query()) .query( @@ -155,6 +162,8 @@ pub mod builders { .map(|v| ("event_days", v.to_string())) .to_query(), ) + .send() + .await?) } } @@ -197,14 +206,17 @@ pub mod builders { self } - pub(crate) fn build(&self, client: &Client) -> RequestBuilder { - client - .get("https://osu.ppy.sh/api/get_scores") + pub(crate) async fn build(&self, client: &Client) -> Result { + Ok(client + .build_request("https://osu.ppy.sh/api/get_scores") + .await? .query(&[("b", self.beatmap_id)]) .query(&self.user.to_query()) .query(&self.mode.to_query()) .query(&self.mods.to_query()) .query(&self.limit.map(|v| ("limit", v.to_string())).to_query()) + .send() + .await?) } } @@ -240,15 +252,18 @@ pub mod builders { self } - pub(crate) fn build(&self, client: &Client) -> RequestBuilder { - client - .get(match self.score_type { + pub(crate) async fn build(&self, client: &Client) -> Result { + Ok(client + .build_request(match self.score_type { UserScoreType::Best => "https://osu.ppy.sh/api/get_user_best", UserScoreType::Recent => "https://osu.ppy.sh/api/get_user_recent", }) + .await? .query(&self.user.to_query()) .query(&self.mode.to_query()) .query(&self.limit.map(|v| ("limit", v.to_string())).to_query()) + .send() + .await?) } } } diff --git a/youmubot-prelude/Cargo.toml b/youmubot-prelude/Cargo.toml index bc2cf59..fbb99ba 100644 --- a/youmubot-prelude/Cargo.toml +++ b/youmubot-prelude/Cargo.toml @@ -7,9 +7,16 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serenity = "0.8" +anyhow = "1.0" +async-trait = "0.1" +futures-util = "0.3" +tokio = { version = "0.2", features = ["time"] } youmubot-db = { path = "../youmubot-db" } -crossbeam-channel = "0.4" reqwest = "0.10" -rayon = "1" chrono = "0.4" +flume = "0.9" + +[dependencies.serenity] +version = "0.9.0-rc.0" +default-features = true +features = ["collector"] diff --git a/youmubot-prelude/src/announcer.rs b/youmubot-prelude/src/announcer.rs index ad5bfb9..ef6693e 100644 --- a/youmubot-prelude/src/announcer.rs +++ b/youmubot-prelude/src/announcer.rs @@ -1,10 +1,13 @@ -use crate::{AppData, GetCloned}; -use crossbeam_channel::after; -use rayon::prelude::*; +use crate::{AppData, Result}; +use async_trait::async_trait; +use futures_util::{ + future::{join_all, ready, FutureExt}, + stream::{FuturesUnordered, StreamExt}, +}; use serenity::{ framework::standard::{ macros::{command, group}, - Args, CommandError as Error, CommandResult, + Args, CommandResult, }, http::CacheHttp, model::{ @@ -15,11 +18,7 @@ use serenity::{ utils::MessageBuilder, CacheAndHttp, }; -use std::{ - collections::HashMap, - sync::Arc, - thread::{spawn, JoinHandle}, -}; +use std::{collections::HashMap, sync::Arc}; use youmubot_db::DB; /// A list of assigned channels for an announcer. @@ -33,30 +32,17 @@ pub(crate) type AnnouncerChannels = DB, d: AppData, channels: MemberToChannels, - ) -> CommandResult; -} - -impl Announcer for T -where - T: FnMut(Arc, AppData, MemberToChannels) -> CommandResult + Send, -{ - fn updates( - &mut self, - c: Arc, - d: AppData, - channels: MemberToChannels, - ) -> CommandResult { - self(c, d, channels) - } + ) -> Result<()>; } /// A simple struct that allows looking up the relevant channels to an user. @@ -64,18 +50,24 @@ pub struct MemberToChannels(Vec<(GuildId, ChannelId)>); impl MemberToChannels { /// Gets the channel list of an user related to that channel. - pub fn channels_of( + pub async fn channels_of( &self, http: impl CacheHttp + Clone + Sync, u: impl Into, ) -> Vec { - let u = u.into(); + let u: UserId = u.into(); self.0 - .par_iter() - .filter_map(|(guild, channel)| { - guild.member(http.clone(), u).ok().map(|_| channel.clone()) + .clone() + .into_iter() + .map(|(guild, channel): (GuildId, ChannelId)| { + guild + .member(http.clone(), u) + .map(move |v| v.ok().map(|_| channel.clone())) }) - .collect::>() + .collect::>() + .filter_map(|v| ready(v)) + .collect() + .await } } @@ -85,7 +77,7 @@ impl MemberToChannels { pub struct AnnouncerHandler { cache_http: Arc, data: AppData, - announcers: HashMap<&'static str, Box>, + announcers: HashMap<&'static str, RwLock>>, } // Querying for the AnnouncerHandler in the internal data returns a vec of keys. @@ -107,8 +99,15 @@ impl AnnouncerHandler { /// Insert a new announcer into the handler. /// /// The handler must take an unique key. If a duplicate is found, this method panics. - pub fn add(&mut self, key: &'static str, announcer: impl Announcer + 'static) -> &mut Self { - if let Some(_) = self.announcers.insert(key, Box::new(announcer)) { + pub fn add( + &mut self, + key: &'static str, + announcer: impl Announcer + Send + Sync + 'static, + ) -> &mut Self { + if let Some(_) = self + .announcers + .insert(key, RwLock::new(Box::new(announcer))) + { panic!( "Announcer keys must be unique: another announcer with key `{}` was found", key @@ -122,9 +121,8 @@ impl AnnouncerHandler { /// Execution-related. impl AnnouncerHandler { /// Collect the list of guilds and their respective channels, by the key of the announcer. - fn get_guilds(&self, key: &'static str) -> Result, Error> { - let d = &self.data; - let data = AnnouncerChannels::open(&*d.read()) + async fn get_guilds(data: &AppData, key: &'static str) -> Result> { + let data = AnnouncerChannels::open(&*data.read().await) .borrow()? .get(key) .map(|m| m.iter().map(|(a, b)| (*a, *b)).collect()) @@ -133,48 +131,55 @@ impl AnnouncerHandler { } /// Run the announcing sequence on a certain announcer. - fn announce(&mut self, key: &'static str) -> CommandResult { - let guilds: Vec<_> = self.get_guilds(key)?; - let channels = MemberToChannels(guilds); - let cache_http = self.cache_http.clone(); - let data = self.data.clone(); - let announcer = self - .announcers - .get_mut(&key) - .expect("Key is from announcers"); - announcer.updates(cache_http, data, channels)?; - Ok(()) + async fn announce( + data: AppData, + cache_http: Arc, + key: &'static str, + announcer: &'_ RwLock>, + ) -> Result<()> { + let channels = MemberToChannels(Self::get_guilds(&data, key).await?); + announcer + .write() + .await + .updates(cache_http, data, channels) + .await } - /// Start the AnnouncerHandler, moving it into another thread. + /// Start the AnnouncerHandler, looping forever. /// /// It will run all the announcers in sequence every *cooldown* seconds. - pub fn scan(mut self, cooldown: std::time::Duration) -> JoinHandle<()> { + pub async fn scan(self, cooldown: std::time::Duration) -> () { // First we store all the keys inside the database. let keys = self.announcers.keys().cloned().collect::>(); - self.data.write().insert::(keys.clone()); - spawn(move || loop { + self.data.write().await.insert::(keys.clone()); + loop { eprintln!("{}: announcer started scanning", chrono::Utc::now()); - let after_timer = after(cooldown); - for key in &keys { + // let after_timer = after(cooldown); + let after = tokio::time::delay_for(cooldown); + join_all(self.announcers.iter().map(|(key, announcer)| { eprintln!(" - scanning key `{}`", key); - if let Err(e) = self.announce(key) { - dbg!(e); - } - } + Self::announce(self.data.clone(), self.cache_http.clone(), *key, announcer).map( + move |v| { + if let Err(e) = v { + eprintln!(" - key `{}`: {:?}", *key, e) + } + }, + ) + })) + .await; eprintln!("{}: announcer finished scanning", chrono::Utc::now()); - after_timer.recv().ok(); - }) + after.await; + } } } /// Gets the announcer of the given guild. -pub fn announcer_of( +pub async fn announcer_of( ctx: &Context, key: &'static str, guild: GuildId, -) -> Result, Error> { - Ok(AnnouncerChannels::open(&*ctx.data.read()) +) -> Result> { + Ok(AnnouncerChannels::open(&*ctx.data.read().await) .borrow()? .get(key) .and_then(|channels| channels.get(&guild).cloned())) @@ -184,20 +189,20 @@ pub fn announcer_of( #[description = "List the registered announcers of this server"] #[num_args(0)] #[only_in(guilds)] -pub fn list_announcers(ctx: &mut Context, m: &Message, _: Args) -> CommandResult { +pub async fn list_announcers(ctx: &Context, m: &Message, _: Args) -> CommandResult { let guild_id = m.guild_id.unwrap(); - let announcers = AnnouncerChannels::open(&*ctx.data.read()); - let announcers = announcers.borrow()?; - - let channels = ctx - .data - .get_cloned::() - .into_iter() - .filter_map(|key| { - announcers - .get(key) - .and_then(|channels| channels.get(&guild_id)) - .map(|&ch| (key, ch)) + let data = &*ctx.data.read().await; + let announcers = AnnouncerChannels::open(data); + let channels = data.get::().unwrap(); + let channels = channels + .iter() + .filter_map(|&key| { + announcers.borrow().ok().and_then(|announcers| { + announcers + .get(key) + .and_then(|channels| channels.get(&guild_id)) + .map(|&ch| (key, ch)) + }) }) .map(|(key, ch)| format!(" - `{}`: activated on channel {}", key, ch.mention())) .collect::>(); @@ -208,7 +213,8 @@ pub fn list_announcers(ctx: &mut Context, m: &Message, _: Args) -> CommandResult "Activated announcers on this server:\n{}", channels.join("\n") ), - )?; + ) + .await?; Ok(()) } @@ -219,23 +225,24 @@ pub fn list_announcers(ctx: &mut Context, m: &Message, _: Args) -> CommandResult #[required_permissions(MANAGE_CHANNELS)] #[only_in(guilds)] #[num_args(1)] -pub fn register_announcer(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +pub async fn register_announcer(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let key = args.single::()?; - let keys = ctx.data.get_cloned::(); - if !keys.contains(&key.as_str()) { + let keys = data.get::().unwrap(); + if !keys.contains(&&key[..]) { m.reply( &ctx, format!( "Key not found. Available announcer keys are: `{}`", keys.join(", ") ), - )?; + ) + .await?; return Ok(()); } - let guild = m.guild(&ctx).expect("Guild-only command"); - let guild = guild.read(); - let channel = m.channel_id.to_channel(&ctx)?; - AnnouncerChannels::open(&*ctx.data.read()) + let guild = m.guild(&ctx).await.expect("Guild-only command"); + let channel = m.channel_id.to_channel(&ctx).await?; + AnnouncerChannels::open(&*data) .borrow_mut()? .entry(key.clone()) .or_default() @@ -250,7 +257,8 @@ pub fn register_announcer(ctx: &mut Context, m: &Message, mut args: Args) -> Com .push(" on channel ") .push_bold_safe(channel) .build(), - )?; + ) + .await?; Ok(()) } @@ -260,9 +268,10 @@ pub fn register_announcer(ctx: &mut Context, m: &Message, mut args: Args) -> Com #[required_permissions(MANAGE_CHANNELS)] #[only_in(guilds)] #[num_args(1)] -pub fn remove_announcer(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { +pub async fn remove_announcer(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; let key = args.single::()?; - let keys = ctx.data.get_cloned::(); + let keys = data.get::().unwrap(); if !keys.contains(&key.as_str()) { m.reply( &ctx, @@ -270,12 +279,12 @@ pub fn remove_announcer(ctx: &mut Context, m: &Message, mut args: Args) -> Comma "Key not found. Available announcer keys are: `{}`", keys.join(", ") ), - )?; + ) + .await?; return Ok(()); } - let guild = m.guild(&ctx).expect("Guild-only command"); - let guild = guild.read(); - AnnouncerChannels::open(&*ctx.data.read()) + let guild = m.guild(&ctx).await.expect("Guild-only command"); + AnnouncerChannels::open(&*data) .borrow_mut()? .entry(key.clone()) .and_modify(|m| { @@ -289,7 +298,8 @@ pub fn remove_announcer(ctx: &mut Context, m: &Message, mut args: Args) -> Comma .push(" has been de-activated for server ") .push_bold_safe(&guild.name) .build(), - )?; + ) + .await?; Ok(()) } diff --git a/youmubot-prelude/src/args.rs b/youmubot-prelude/src/args.rs index cdb5979..80175d7 100644 --- a/youmubot-prelude/src/args.rs +++ b/youmubot-prelude/src/args.rs @@ -2,14 +2,17 @@ pub use duration::Duration; pub use username_arg::UsernameArg; mod duration { + use crate::{Error, Result}; use std::fmt; use std::time::Duration as StdDuration; - use String as Error; - // Parse a single duration unit - fn parse_duration_string(s: &str) -> Result { + + const INVALID_DURATION: &str = "Not a valid duration"; + + /// Parse a single duration unit + fn parse_duration_string(s: &str) -> Result { // We reject the empty case if s == "" { - return Err(Error::from("empty strings are not valid durations")); + return Err(Error::msg("empty strings are not valid durations")); } struct ParseStep { current_value: Option, @@ -26,7 +29,7 @@ mod duration { current_value: Some(v.unwrap_or(0) * 10 + ((item as u64) - ('0' as u64))), ..s }), - (_, None) => Err(Error::from("Not a valid duration")), + (_, None) => Err(Error::msg(INVALID_DURATION)), (item, Some(v)) => Ok(ParseStep { current_value: None, current_duration: s.current_duration @@ -36,7 +39,7 @@ mod duration { 'h' => StdDuration::from_secs(60 * 60), 'd' => StdDuration::from_secs(60 * 60 * 24), 'w' => StdDuration::from_secs(60 * 60 * 24 * 7), - _ => return Err(Error::from("Not a valid duration")), + _ => return Err(Error::msg(INVALID_DURATION)), } * (v as u32), }), }, @@ -44,7 +47,7 @@ mod duration { .and_then(|v| match v.current_value { // All values should be consumed None => Ok(v), - _ => Err(Error::from("Not a valid duration")), + _ => Err(Error::msg(INVALID_DURATION)), }) .map(|v| v.current_duration) } diff --git a/youmubot-prelude/src/hook.rs b/youmubot-prelude/src/hook.rs new file mode 100644 index 0000000..5760b17 --- /dev/null +++ b/youmubot-prelude/src/hook.rs @@ -0,0 +1,24 @@ +use crate::{async_trait, future, Context, Result}; +use serenity::model::channel::Message; + +/// Hook represents the asynchronous hook that is run on every message. +#[async_trait] +pub trait Hook: Send + Sync { + async fn call(&mut self, ctx: &Context, message: &Message) -> Result<()>; +} + +#[async_trait] +impl Hook for T +where + T: for<'a> FnMut( + &'a Context, + &'a Message, + ) + -> std::pin::Pin> + 'a + Send>> + + Send + + Sync, +{ + async fn call(&mut self, ctx: &Context, message: &Message) -> Result<()> { + self(ctx, message).await + } +} diff --git a/youmubot-prelude/src/lib.rs b/youmubot-prelude/src/lib.rs index 4b72eac..4d7a8ac 100644 --- a/youmubot-prelude/src/lib.rs +++ b/youmubot-prelude/src/lib.rs @@ -1,54 +1,40 @@ +/// Module `prelude` provides a sane set of default imports that can be used inside +/// a Youmubot source file. pub use serenity::prelude::*; use std::sync::Arc; pub mod announcer; pub mod args; +pub mod hook; pub mod pagination; -pub mod reaction_watch; +pub mod ratelimit; pub mod setup; pub use announcer::{Announcer, AnnouncerHandler}; pub use args::{Duration, UsernameArg}; -pub use pagination::Pagination; -pub use reaction_watch::{ReactionHandler, ReactionWatcher}; +pub use hook::Hook; +pub use pagination::paginate; + +/// Re-exporting async_trait helps with implementing Announcer. +pub use async_trait::async_trait; + +/// Re-export the anyhow errors +pub use anyhow::{Error, Result}; + +/// Re-export useful future and stream utils +pub use futures_util::{future, stream, FutureExt, StreamExt, TryFutureExt, TryStreamExt}; + +/// Re-export the spawn function +pub use tokio::spawn as spawn_future; /// The global app data. -pub type AppData = Arc>; +pub type AppData = Arc>; /// The HTTP client. pub struct HTTPClient; impl TypeMapKey for HTTPClient { - type Value = reqwest::blocking::Client; -} - -/// The TypeMap trait that allows TypeMaps to quickly get a clonable item. -pub trait GetCloned { - /// Gets an item from the store, cloned. - fn get_cloned(&self) -> T::Value - where - T: TypeMapKey, - T::Value: Clone + Send + Sync; -} - -impl GetCloned for ShareMap { - fn get_cloned(&self) -> T::Value - where - T: TypeMapKey, - T::Value: Clone + Send + Sync, - { - self.get::().cloned().expect("Should be there") - } -} - -impl GetCloned for AppData { - fn get_cloned(&self) -> T::Value - where - T: TypeMapKey, - T::Value: Clone + Send + Sync, - { - self.read().get::().cloned().expect("Should be there") - } + type Value = reqwest::Client; } pub mod prelude_commands { @@ -70,8 +56,8 @@ pub mod prelude_commands { #[command] #[description = "pong!"] - fn ping(ctx: &mut Context, m: &Message) -> CommandResult { - m.reply(&ctx, "Pong!")?; + async fn ping(ctx: &Context, m: &Message) -> CommandResult { + m.reply(&ctx, "Pong!").await?; Ok(()) } } diff --git a/youmubot-prelude/src/pagination.rs b/youmubot-prelude/src/pagination.rs index 26a591e..c16802e 100644 --- a/youmubot-prelude/src/pagination.rs +++ b/youmubot-prelude/src/pagination.rs @@ -1,157 +1,111 @@ -use crate::{Context, ReactionHandler, ReactionWatcher}; +use crate::{Context, Result}; +use futures_util::{future::Future, StreamExt}; use serenity::{ - builder::EditMessage, - framework::standard::{CommandError, CommandResult}, + collector::ReactionAction, model::{ - channel::{Message, Reaction, ReactionType}, + channel::{Message, ReactionType}, id::ChannelId, }, }; +use std::convert::TryFrom; +use tokio::time as tokio_time; const ARROW_RIGHT: &'static str = "➡️"; const ARROW_LEFT: &'static str = "⬅️"; -impl ReactionWatcher { - /// Start a pagination. - /// - /// Takes a copy of Context (which you can `clone`), a pager (see "Pagination") and a target channel id. - /// Pagination will handle all events on adding/removing an "arrow" emoji (⬅️ and ➡️). - /// This is a blocking call - it will block the thread until duration is over. - pub fn paginate( - &self, - ctx: Context, - channel: ChannelId, - pager: T, - duration: std::time::Duration, - ) -> CommandResult { - let handler = PaginationHandler::new(pager, ctx, channel)?; - self.handle_reactions(handler, duration, |_| {}); - Ok(()) - } - - /// A version of `paginate` that compiles for closures. - /// - /// A workaround until https://github.com/rust-lang/rust/issues/36582 is solved. - pub fn paginate_fn( - &self, - ctx: Context, - channel: ChannelId, - pager: T, - duration: std::time::Duration, - ) -> CommandResult - where - T: for<'a> FnMut(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult) - + Send - + 'static, - { - self.paginate(ctx, channel, pager, duration) - } +#[async_trait::async_trait] +pub trait Paginate { + async fn render(&mut self, page: u8, ctx: &Context, m: &mut Message) -> Result; } -/// Pagination allows the bot to display content in multiple pages. -/// -/// You need to implement the "render_page" function, which takes a dummy content and -/// embed assigning function. -/// Pagination is automatically implemented for functions with the same signature as `render_page`. -/// -/// Pages start at 0. -pub trait Pagination { - /// Render a page. - /// - /// This would either create or edit a message, but you should not be worry about it. - fn render_page<'a>( - &mut self, - page: u8, - target: &'a mut EditMessage, - ) -> (&'a mut EditMessage, CommandResult); -} - -impl Pagination for T +#[async_trait::async_trait] +impl Paginate for T where - T: for<'a> FnMut(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult), + T: for<'m> FnMut( + u8, + &'m Context, + &'m mut Message, + ) -> std::pin::Pin> + Send + 'm>> + + Send, { - fn render_page<'a>( - &mut self, - page: u8, - target: &'a mut EditMessage, - ) -> (&'a mut EditMessage, CommandResult) { - self(page, target) + async fn render(&mut self, page: u8, ctx: &Context, m: &mut Message) -> Result { + self(page, ctx, m).await } } -struct PaginationHandler { - pager: T, - message: Message, +// Paginate! with a pager function. +/// If awaited, will block until everything is done. +pub async fn paginate( + mut pager: impl for<'m> FnMut( + u8, + &'m Context, + &'m mut Message, + ) -> std::pin::Pin> + Send + 'm>> + + Send, + ctx: &Context, + channel: ChannelId, + timeout: std::time::Duration, +) -> Result<()> { + let mut message = channel + .send_message(&ctx, |e| e.content("Youmu is loading the first page...")) + .await?; + // React to the message + message + .react(&ctx, ReactionType::try_from(ARROW_LEFT)?) + .await?; + message + .react(&ctx, ReactionType::try_from(ARROW_RIGHT)?) + .await?; + pager(0, ctx, &mut message).await?; + // Build a reaction collector + let mut reaction_collector = message.await_reactions(&ctx).removed(true).await; + let mut page = 0; + + // Loop the handler function. + let res: Result<()> = loop { + match tokio_time::timeout(timeout, reaction_collector.next()).await { + Err(_) => break Ok(()), + Ok(None) => break Ok(()), + Ok(Some(reaction)) => { + page = match handle_reaction(page, &mut pager, ctx, &mut message, &reaction).await { + Ok(v) => v, + Err(e) => break Err(e), + }; + } + } + }; + + message.react(&ctx, '🛑').await?; + + res +} + +// Handle the reaction and return a new page number. +async fn handle_reaction( page: u8, - ctx: Context, -} - -impl PaginationHandler { - pub fn new(pager: T, mut ctx: Context, channel: ChannelId) -> Result { - let message = channel.send_message(&mut ctx, |e| { - e.content("Youmu is loading the first page...") - })?; - // React to the message - message.react(&mut ctx, ARROW_LEFT)?; - message.react(&mut ctx, ARROW_RIGHT)?; - let mut p = Self { - pager, - message: message.clone(), - page: 0, - ctx, - }; - p.call_pager()?; - Ok(p) - } -} - -impl PaginationHandler { - /// Call the pager, log the error (if any). - fn call_pager(&mut self) -> CommandResult { - let mut res: CommandResult = Ok(()); - let mut msg = self.message.clone(); - msg.edit(self.ctx.http.clone(), |e| { - let (e, r) = self.pager.render_page(self.page, e); - res = r; - e - })?; - self.message = msg; - res - } -} - -impl Drop for PaginationHandler { - fn drop(&mut self) { - self.message.react(&self.ctx, "🛑").ok(); - } -} - -impl ReactionHandler for PaginationHandler { - fn handle_reaction(&mut self, reaction: &Reaction, _is_add: bool) -> CommandResult { - if reaction.message_id != self.message.id { - return Ok(()); - } - match &reaction.emoji { - ReactionType::Unicode(ref s) => match s.as_str() { - ARROW_LEFT if self.page == 0 => return Ok(()), - ARROW_LEFT => { - self.page -= 1; - if let Err(e) = self.call_pager() { - self.page += 1; - return Err(e); - } - } - ARROW_RIGHT => { - self.page += 1; - if let Err(e) = self.call_pager() { - self.page -= 1; - return Err(e); - } - } - _ => (), - }, - _ => (), - } - Ok(()) + pager: &mut impl Paginate, + ctx: &Context, + message: &mut Message, + reaction: &ReactionAction, +) -> Result { + let reaction = match reaction { + ReactionAction::Added(v) | ReactionAction::Removed(v) => v, + }; + match &reaction.emoji { + ReactionType::Unicode(ref s) => match s.as_str() { + ARROW_LEFT if page == 0 => Ok(page), + ARROW_LEFT => Ok(if pager.render(page - 1, ctx, message).await? { + page - 1 + } else { + page + }), + ARROW_RIGHT => Ok(if pager.render(page + 1, ctx, message).await? { + page + 1 + } else { + page + }), + _ => Ok(page), + }, + _ => Ok(page), } } diff --git a/youmubot-prelude/src/ratelimit.rs b/youmubot-prelude/src/ratelimit.rs new file mode 100644 index 0000000..f9ad47b --- /dev/null +++ b/youmubot-prelude/src/ratelimit.rs @@ -0,0 +1,67 @@ +/// Provides a simple ratelimit lock (that only works in tokio) +// use tokio::time:: +use std::time::Duration; + +use crate::Result; +use flume::{bounded as channel, Receiver, Sender}; +use std::ops::Deref; + +/// Holds the underlying `T` in a rate-limited way. +pub struct Ratelimit { + inner: T, + recv: Receiver<()>, + send: Sender<()>, + + wait_time: Duration, +} + +struct RatelimitGuard<'a, T> { + inner: &'a T, + send: &'a Sender<()>, + wait_time: &'a Duration, +} + +impl Ratelimit { + /// Create a new ratelimit with at most `count` uses in `wait_time`. + pub fn new(inner: T, count: usize, wait_time: Duration) -> Self { + let (send, recv) = channel(count); + (0..count).for_each(|_| { + send.send(()).ok(); + }); + Self { + inner, + send, + recv, + wait_time, + } + } + + /// Borrow the inner `T`. You can only hol this reference `count` times in `wait_time`. + /// The clock counts from the moment the ref is dropped. + pub async fn borrow<'a>(&'a self) -> Result + 'a> { + self.recv.recv_async().await?; + Ok(RatelimitGuard { + inner: &self.inner, + send: &self.send, + wait_time: &self.wait_time, + }) + } +} + +impl<'a, T> Deref for RatelimitGuard<'a, T> { + type Target = T; + fn deref(&self) -> &Self::Target { + self.inner + } +} + +impl<'a, T> Drop for RatelimitGuard<'a, T> { + fn drop(&mut self) { + let send = self.send.clone(); + let wait_time = self.wait_time.clone(); + tokio::spawn(async move { + tokio::time::delay_for(wait_time).await; + send.send_async(()).await.ok(); + }); + } +} diff --git a/youmubot-prelude/src/reaction_watch.rs b/youmubot-prelude/src/reaction_watch.rs deleted file mode 100644 index 9cd5d6f..0000000 --- a/youmubot-prelude/src/reaction_watch.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crossbeam_channel::{after, bounded, select, Sender}; -use serenity::{framework::standard::CommandResult, model::channel::Reaction, prelude::*}; -use std::sync::{Arc, Mutex}; - -/// Handles a reaction. -/// -/// Every handler needs an expire time too. -pub trait ReactionHandler { - /// Handle a reaction. This is fired on EVERY reaction. - /// You do the filtering yourself. - /// - /// If `is_added` is false, the reaction was removed instead of added. - fn handle_reaction(&mut self, reaction: &Reaction, is_added: bool) -> CommandResult; -} - -impl ReactionHandler for T -where - T: FnMut(&Reaction, bool) -> CommandResult, -{ - fn handle_reaction(&mut self, reaction: &Reaction, is_added: bool) -> CommandResult { - self(reaction, is_added) - } -} - -/// The store for a set of dynamic reaction handlers. -#[derive(Debug, Clone)] -pub struct ReactionWatcher { - channels: Arc, bool)>>>>, -} - -impl TypeMapKey for ReactionWatcher { - type Value = ReactionWatcher; -} - -impl ReactionWatcher { - /// Create a new ReactionWatcher. - pub fn new() -> Self { - Self { - channels: Arc::new(Mutex::new(vec![])), - } - } - /// Send a reaction. - /// If `is_added` is false, the reaction was removed. - pub fn send(&self, r: Reaction, is_added: bool) { - let r = Arc::new(r); - self.channels - .lock() - .expect("Poisoned!") - .retain(|e| e.send((r.clone(), is_added)).is_ok()); - } - /// React! to a series of reaction - /// - /// The reactions stop after `duration` of idle. - pub fn handle_reactions( - &self, - mut h: H, - duration: std::time::Duration, - callback: impl FnOnce(H) -> () + Send + 'static, - ) { - let (send, reactions) = bounded(0); - { - self.channels.lock().expect("Poisoned!").push(send); - } - std::thread::spawn(move || { - loop { - let timeout = after(duration); - let r = select! { - recv(reactions) -> r => { let (r, is_added) = r.unwrap(); h.handle_reaction(&*r, is_added) }, - recv(timeout) -> _ => break, - }; - if let Err(v) = r { - dbg!(v); - } - } - callback(h) - }); - } - /// React! to a series of reaction - /// - /// The handler will stop after `duration` no matter what. - pub fn handle_reactions_timed( - &self, - mut h: H, - duration: std::time::Duration, - callback: impl FnOnce(H) -> () + Send + 'static, - ) { - let (send, reactions) = bounded(0); - { - self.channels.lock().expect("Poisoned!").push(send); - } - std::thread::spawn(move || { - let timeout = after(duration); - loop { - let r = select! { - recv(reactions) -> r => { let (r, is_added) = r.unwrap(); h.handle_reaction(&*r, is_added) }, - recv(timeout) -> _ => break, - }; - if let Err(v) = r { - dbg!(v); - } - } - callback(h); - }); - } -} diff --git a/youmubot-prelude/src/setup.rs b/youmubot-prelude/src/setup.rs index ab523cc..2d3704c 100644 --- a/youmubot-prelude/src/setup.rs +++ b/youmubot-prelude/src/setup.rs @@ -1,17 +1,14 @@ -use serenity::{framework::standard::StandardFramework, prelude::*}; +use serenity::prelude::*; use std::path::Path; /// Set up the prelude libraries. /// /// Panics on failure: Youmubot should *NOT* attempt to continue when this function fails. -pub fn setup_prelude(db_path: &Path, data: &mut ShareMap, _: &mut StandardFramework) { +pub fn setup_prelude(db_path: &Path, data: &mut TypeMap) { // Setup the announcer DB. crate::announcer::AnnouncerChannels::insert_into(data, db_path.join("announcers.yaml")) .expect("Announcers DB set up"); // Set up the HTTP client. - data.insert::(reqwest::blocking::Client::new()); - - // Set up the reaction watcher. - data.insert::(crate::ReactionWatcher::new()); + data.insert::(reqwest::Client::new()); } diff --git a/youmubot/Cargo.toml b/youmubot/Cargo.toml index c20d200..3280a04 100644 --- a/youmubot/Cargo.toml +++ b/youmubot/Cargo.toml @@ -12,8 +12,10 @@ osu = ["youmubot-osu"] codeforces = ["youmubot-cf"] [dependencies] -serenity = "0.8" +serenity = "0.9.0-rc.0" +tokio = "0.2" dotenv = "0.15" +env_logger = "0.7" youmubot-db = { path = "../youmubot-db" } youmubot-prelude = { path = "../youmubot-prelude" } youmubot-core = { path = "../youmubot-core" } diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index cb21aa0..d1a008b 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -1,9 +1,10 @@ use dotenv; use dotenv::var; use serenity::{ - framework::standard::{DispatchError, StandardFramework}, + client::bridge::gateway::GatewayIntents, + framework::standard::{macros::hook, CommandResult, DispatchError, StandardFramework}, model::{ - channel::{Channel, Message, Reaction}, + channel::{Channel, Message}, gateway, id::{ChannelId, GuildId, UserId}, permissions::Permissions, @@ -12,51 +13,59 @@ use serenity::{ use youmubot_prelude::*; struct Handler { - hooks: Vec ()>, + hooks: Vec>>, } impl Handler { fn new() -> Handler { Handler { hooks: vec![] } } + + fn push_hook(&mut self, f: T) { + self.hooks.push(RwLock::new(Box::new(f))); + } } +#[async_trait] impl EventHandler for Handler { - fn ready(&self, _: Context, ready: gateway::Ready) { + async fn ready(&self, _: Context, ready: gateway::Ready) { println!("{} is connected!", ready.user.name); } - fn message(&self, mut ctx: Context, message: Message) { - self.hooks.iter().for_each(|f| f(&mut ctx, &message)); - } - - fn reaction_add(&self, ctx: Context, reaction: Reaction) { - ctx.data - .get_cloned::() - .send(reaction, true); - } - - fn reaction_remove(&self, ctx: Context, reaction: Reaction) { - ctx.data - .get_cloned::() - .send(reaction, false); + async fn message(&self, ctx: Context, message: Message) { + self.hooks + .iter() + .map(|hook| { + let ctx = ctx.clone(); + let message = message.clone(); + hook.write() + .then(|mut h| async move { h.call(&ctx, &message).await }) + }) + .collect::>() + .for_each(|v| async move { + if let Err(e) = v { + eprintln!("{}", e) + } + }) + .await; } } /// Returns whether the user has "MANAGE_MESSAGES" permission in the channel. -fn is_channel_mod(ctx: &mut Context, _: Option, ch: ChannelId, u: UserId) -> bool { - match ch.to_channel(&ctx) { - Ok(Channel::Guild(gc)) => { - let gc = gc.read(); - gc.permissions_for_user(&ctx, u) - .map(|perms| perms.contains(Permissions::MANAGE_MESSAGES)) - .unwrap_or(false) - } +async fn is_channel_mod(ctx: &Context, _: Option, ch: ChannelId, u: UserId) -> bool { + match ch.to_channel(&ctx).await { + Ok(Channel::Guild(gc)) => gc + .permissions_for_user(&ctx, u) + .await + .map(|perms| perms.contains(Permissions::MANAGE_MESSAGES)) + .unwrap_or(false), _ => false, } } -fn main() { +#[tokio::main] +async fn main() { + env_logger::init(); // Setup dotenv if let Ok(path) = dotenv::dotenv() { println!("Loaded dotenv from {:?}", path); @@ -65,34 +74,48 @@ fn main() { let mut handler = Handler::new(); // Set up hooks #[cfg(feature = "osu")] - handler.hooks.push(youmubot_osu::discord::hook); + handler.push_hook(youmubot_osu::discord::hook); #[cfg(feature = "codeforces")] - handler.hooks.push(youmubot_cf::codeforces_info_hook); + handler.push_hook(youmubot_cf::InfoHook); + + // Collect the token + let token = var("TOKEN").expect("Please set TOKEN as the Discord Bot's token to be used."); + // Set up base framework + let fw = setup_framework(&token[..]).await; // Sets up a client let mut client = { - // Collect the token - let token = var("TOKEN").expect("Please set TOKEN as the Discord Bot's token to be used."); // Attempt to connect and set up a framework - Client::new(token, handler).expect("Cannot connect") + Client::new(token) + .framework(fw) + .event_handler(handler) + .intents( + GatewayIntents::GUILDS + | GatewayIntents::GUILD_BANS + | GatewayIntents::GUILD_MESSAGES + | GatewayIntents::GUILD_MESSAGE_REACTIONS + | GatewayIntents::GUILD_PRESENCES + | GatewayIntents::GUILD_MEMBERS + | GatewayIntents::DIRECT_MESSAGES + | GatewayIntents::DIRECT_MESSAGE_REACTIONS, + ) + .await + .unwrap() }; - // Set up base framework - let mut fw = setup_framework(&client); - // Set up announcer handler let mut announcers = AnnouncerHandler::new(&client); // Setup each package starting from the prelude. { - let mut data = client.data.write(); + let mut data = client.data.write().await; let db_path = var("DBPATH") .map(|v| std::path::PathBuf::from(v)) .unwrap_or_else(|e| { println!("No DBPATH set up ({:?}), using `/data`", e); std::path::PathBuf::from("data") }); - youmubot_prelude::setup::setup_prelude(&db_path, &mut data, &mut fw); + youmubot_prelude::setup::setup_prelude(&db_path, &mut data); // Setup core #[cfg(feature = "core")] youmubot_core::setup(&db_path, &client, &mut data).expect("Setup db should succeed"); @@ -102,7 +125,7 @@ fn main() { .expect("osu! is initialized"); // codeforces #[cfg(feature = "codeforces")] - youmubot_cf::setup(&db_path, &mut data, &mut announcers); + youmubot_cf::setup(&db_path, &mut data, &mut announcers).await; } #[cfg(feature = "core")] @@ -112,11 +135,10 @@ fn main() { #[cfg(feature = "codeforces")] println!("codeforces enabled."); - client.with_framework(fw); - announcers.scan(std::time::Duration::from_secs(120)); + tokio::spawn(announcers.scan(std::time::Duration::from_secs(120))); println!("Starting..."); - if let Err(v) = client.start() { + if let Err(v) = client.start().await { panic!(v) } @@ -124,71 +146,43 @@ fn main() { } // Sets up a framework for a client -fn setup_framework(client: &Client) -> StandardFramework { +async fn setup_framework(token: &str) -> StandardFramework { + let http = serenity::http::Http::new_with_token(token); // Collect owners - let owner = client - .cache_and_http - .http + let owner = http .get_current_application_info() + .await .expect("Should be able to get app info") .owner; - let fw = StandardFramework::new() - .configure(|c| { - c.with_whitespace(false) - .prefix(&var("PREFIX").unwrap_or("y!".to_owned())) - .delimiters(vec![" / ", "/ ", " /", "/"]) - .owners([owner.id].iter().cloned().collect()) - }) - .help(&youmubot_core::HELP) - .before(|_, msg, command_name| { - println!( - "Got command '{}' by user '{}'", - command_name, msg.author.name - ); - true - }) - .after(|ctx, msg, command_name, error| match error { - Ok(()) => println!("Processed command '{}'", command_name), - Err(why) => { - let reply = format!("Command '{}' returned error {:?}", command_name, why); - if let Err(_) = msg.reply(&ctx, &reply) {} - println!("{}", reply) - } - }) - .on_dispatch_error(|ctx, msg, error| { - msg.reply( - &ctx, - &match error { - DispatchError::Ratelimited(seconds) => format!( - "⏳ You are being rate-limited! Try this again in **{} seconds**.", - seconds - ), - DispatchError::NotEnoughArguments { min, given } => format!("😕 The command needs at least **{}** arguments, I only got **{}**!\nDid you know command arguments are separated with a slash (`/`)?", min, given), - DispatchError::TooManyArguments { max, given } => format!("😕 I can only handle at most **{}** arguments, but I got **{}**!", max, given), - DispatchError::OnlyForGuilds => format!("🔇 This command cannot be used in DMs."), - _ => return, - }, - ) - .unwrap(); // Invoke - }) - // Set a function that's called whenever an attempted command-call's - // command could not be found. - .unrecognised_command(|_, _, unknown_command_name| { - println!("Could not find command named '{}'", unknown_command_name); - }) - // Set a function that's called whenever a message is not a command. - .normal_message(|_, _| { - // println!("Message is not a command '{}'", message.content); - }) - .bucket("voting", |c| { - c.check(|ctx, g, ch, u| !is_channel_mod(ctx, g, ch, u)).delay(120 /* 2 minutes */).time_span(120).limit(1) - }) - .bucket("images", |c| c.time_span(60).limit(2)) - .bucket("community", |c| { - c.check(|ctx, g, ch, u| !is_channel_mod(ctx, g, ch, u)).delay(30).time_span(30).limit(1) - }) - .group(&prelude_commands::PRELUDE_GROUP); + let fw = StandardFramework::new() + .configure(|c| { + c.with_whitespace(false) + .prefix(&var("PREFIX").unwrap_or("y!".to_owned())) + .delimiters(vec![" / ", "/ ", " /", "/"]) + .owners([owner.id].iter().cloned().collect()) + }) + .help(&youmubot_core::HELP) + .before(before_hook) + .after(after_hook) + .on_dispatch_error(on_dispatch_error) + .bucket("voting", |c| { + c.check(|ctx, g, ch, u| Box::pin(async move { !is_channel_mod(ctx, g, ch, u).await })) + .delay(120 /* 2 minutes */) + .time_span(120) + .limit(1) + }) + .await + .bucket("images", |c| c.time_span(60).limit(2)) + .await + .bucket("community", |c| { + c.check(|ctx, g, ch, u| Box::pin(async move { !is_channel_mod(ctx, g, ch, u).await })) + .delay(30) + .time_span(30) + .limit(1) + }) + .await + .group(&prelude_commands::PRELUDE_GROUP); // groups here #[cfg(feature = "core")] let fw = fw @@ -201,3 +195,53 @@ fn setup_framework(client: &Client) -> StandardFramework { let fw = fw.group(&youmubot_cf::CODEFORCES_GROUP); fw } + +// Hooks! + +#[hook] +async fn before_hook(_: &Context, msg: &Message, command_name: &str) -> bool { + println!( + "Got command '{}' by user '{}'", + command_name, msg.author.name + ); + true +} + +#[hook] +async fn after_hook(ctx: &Context, msg: &Message, command_name: &str, error: CommandResult) { + match error { + Ok(()) => println!("Processed command '{}'", command_name), + Err(why) => { + let reply = format!("Command '{}' returned error {:?}", command_name, why); + msg.reply(&ctx, &reply).await.ok(); + println!("{}", reply) + } + } +} + +#[hook] +async fn on_dispatch_error(ctx: &Context, msg: &Message, error: DispatchError) { + msg.reply( + &ctx, + &match error { + DispatchError::Ratelimited(seconds) => format!( + "⏳ You are being rate-limited! Try this again in **{}**.", + youmubot_prelude::Duration(seconds), + ), + DispatchError::NotEnoughArguments { min, given } => { + format!( + "😕 The command needs at least **{}** arguments, I only got **{}**!", + min, given + ) + "\nDid you know command arguments are separated with a slash (`/`)?" + } + DispatchError::TooManyArguments { max, given } => format!( + "😕 I can only handle at most **{}** arguments, but I got **{}**!", + max, given + ), + DispatchError::OnlyForGuilds => format!("🔇 This command cannot be used in DMs."), + _ => return, + }, + ) + .await + .ok(); // Invoke +}