From 1799b70bc1221133efdf00b27d7a47803f47b552 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sat, 19 Jun 2021 22:36:17 +0900 Subject: [PATCH] Move to SQLite (#13) --- .github/workflows/build_test.yml | 6 + .github/workflows/deploy.yml | 2 + .gitignore | 2 + Cargo.lock | 454 +++++++++++++++- Cargo.toml | 1 + youmubot-cf/src/announcer.rs | 2 +- youmubot-cf/src/embed.rs | 4 +- youmubot-cf/src/hook.rs | 17 +- youmubot-cf/src/lib.rs | 12 +- youmubot-cf/src/live.rs | 34 +- youmubot-core/src/admin/mod.rs | 4 +- youmubot-core/src/admin/soft_ban.rs | 15 +- youmubot-core/src/community/mod.rs | 4 +- youmubot-core/src/community/roles.rs | 6 +- youmubot-core/src/community/votes.rs | 7 +- youmubot-core/src/fun/mod.rs | 2 +- youmubot-core/src/fun/names.rs | 4 +- youmubot-db-sql/Cargo.toml | 14 + .../migrations/20210329111010_osu_users.sql | 12 + .../migrations/20210331094454_more_osu.sql | 45 ++ youmubot-db-sql/sqlx-data.json | 491 ++++++++++++++++++ youmubot-db-sql/src/lib.rs | 48 ++ youmubot-db-sql/src/models/mod.rs | 22 + youmubot-db-sql/src/models/osu.rs | 319 ++++++++++++ youmubot-db-sql/src/models/osu_user.rs | 118 +++++ youmubot-db/src/lib.rs | 2 +- youmubot-osu/Cargo.toml | 2 + youmubot-osu/src/discord/announcer.rs | 98 ++-- youmubot-osu/src/discord/beatmap_cache.rs | 118 +++-- youmubot-osu/src/discord/cache.rs | 23 +- youmubot-osu/src/discord/db.rs | 209 +++++++- youmubot-osu/src/discord/display.rs | 7 +- youmubot-osu/src/discord/embeds.rs | 23 +- youmubot-osu/src/discord/hook.rs | 1 + youmubot-osu/src/discord/mod.rs | 105 ++-- youmubot-osu/src/discord/oppai_cache.rs | 53 +- youmubot-osu/src/discord/server_rank.rs | 118 ++--- youmubot-osu/src/lib.rs | 2 +- youmubot-osu/src/models/mod.rs | 28 +- youmubot-osu/src/models/mods.rs | 6 +- youmubot-osu/src/models/parse.rs | 2 +- youmubot-prelude/Cargo.toml | 1 + youmubot-prelude/src/announcer.rs | 11 +- youmubot-prelude/src/args.rs | 4 +- youmubot-prelude/src/lib.rs | 7 + youmubot-prelude/src/member_cache.rs | 2 +- youmubot-prelude/src/pagination.rs | 4 +- youmubot-prelude/src/ratelimit.rs | 6 +- youmubot-prelude/src/setup.rs | 21 +- youmubot/src/main.rs | 18 +- 50 files changed, 2122 insertions(+), 394 deletions(-) create mode 100644 youmubot-db-sql/Cargo.toml create mode 100644 youmubot-db-sql/migrations/20210329111010_osu_users.sql create mode 100644 youmubot-db-sql/migrations/20210331094454_more_osu.sql create mode 100644 youmubot-db-sql/sqlx-data.json create mode 100644 youmubot-db-sql/src/lib.rs create mode 100644 youmubot-db-sql/src/models/mod.rs create mode 100644 youmubot-db-sql/src/models/osu.rs create mode 100644 youmubot-db-sql/src/models/osu_user.rs diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 8bffa87..133fcb0 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -40,8 +40,12 @@ jobs: key: ${{ runner.os }}-rust-${{ steps.cargo.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.lock') }}-lint - name: Run cargo check run: cargo check + env: + SQLX_OFFLINE: "true" - name: Run clippy run: cargo clippy + env: + SQLX_OFFLINE: "true" test: name: Test runs-on: ubuntu-latest @@ -61,3 +65,5 @@ jobs: key: ${{ runner.os }}-rust-${{ steps.cargo.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.lock') }}-debug-build - name: Run cargo test run: cargo test + env: + SQLX_OFFLINE: "true" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 94a0a57..e2cb9dd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,6 +24,8 @@ jobs: key: ${{ runner.os }}-rust-${{ steps.cargo.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.lock') }}-release-build - name: Build release run: cargo build --release + env: + SQLX_OFFLINE: "true" - name: Upload compiled binary artifact uses: actions/upload-artifact@v1 with: diff --git a/.gitignore b/.gitignore index 2c7a7fb..4264ecb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ target *.yaml cargo-remote .vscode +youmubot.db +youmubot.db-* diff --git a/Cargo.lock b/Cargo.lock index a125dfb..4661ed2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,6 +16,23 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +[[package]] +name = "ahash" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" + +[[package]] +name = "ahash" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796540673305a66d127804eef19ad696f1f204b8c1025aaca4958c17eab32877" +dependencies = [ + "getrandom 0.2.2", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.15" @@ -31,6 +48,12 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "async-trait" version = "0.1.42" @@ -58,6 +81,15 @@ dependencies = [ "webpki-roots 0.20.0", ] +[[package]] +name = "atoi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" +dependencies = [ + "num-traits", +] + [[package]] name = "atty" version = "0.2.14" @@ -87,12 +119,34 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bincode" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" +dependencies = [ + "byteorder", + "serde", +] + [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bitvec" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -102,6 +156,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "build_const" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" + [[package]] name = "bumpalo" version = "3.6.0" @@ -168,9 +228,9 @@ dependencies = [ [[package]] name = "command_attr" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef54b146e4ff8a036b9d632fd48c183c9757992535e5f557275f4a01dfd9c7c7" +checksum = "8fe1f0b69fde68f40ea2ee6ca8db23bc40d2e593db884659a65d8486032cc65b" dependencies = [ "proc-macro2", "quote", @@ -199,6 +259,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +dependencies = [ + "build_const", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -208,6 +277,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + [[package]] name = "dashmap" version = "4.0.2" @@ -239,6 +339,15 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e" +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.26" @@ -317,6 +426,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "futures" version = "0.3.12" @@ -325,6 +440,7 @@ checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -347,6 +463,17 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" +[[package]] +name = "futures-executor" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.12" @@ -459,6 +586,27 @@ name = "hashbrown" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash 0.4.7", +] + +[[package]] +name = "hashlink" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99cf782f0dc4372d26846bec3de7804ceb5df083c2d4462c0b8d2330e894fa8" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -469,6 +617,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "0.2.3" @@ -593,6 +747,15 @@ dependencies = [ "bytes 0.5.6", ] +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.3.0" @@ -620,12 +783,36 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexical-core" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" +[[package]] +name = "libsqlite3-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d31059f22935e6c31830db5249ba2b7ecd54fd73a9909286f0a67aa55c2fbd" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.4" @@ -650,6 +837,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matches" version = "0.1.8" @@ -738,6 +931,19 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "ntapi" version = "0.3.6" @@ -832,6 +1038,31 @@ dependencies = [ "libc", ] +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -938,6 +1169,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "rand" version = "0.7.3" @@ -1216,6 +1453,7 @@ version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" dependencies = [ + "indexmap", "itoa", "ryu", "serde", @@ -1247,9 +1485,9 @@ dependencies = [ [[package]] name = "serenity" -version = "0.10.2" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dac8367ecfd3380c00dcedf5eb9a47888ae74ae391419b5b1f7735895ed8df4" +checksum = "deead3f7ecbbbe4c249e07af17686937ccb9d7fa24ca3accd1d223e369a75272" dependencies = [ "async-trait", "async-tungstenite", @@ -1267,7 +1505,6 @@ dependencies = [ "static_assertions", "tokio", "tracing", - "tracing-futures", "typemap_rev", "url", "uwl", @@ -1286,12 +1523,31 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" +dependencies = [ + "block-buffer", + "cfg-if", + "cpuid-bool", + "digest", + "opaque-debug", +] + [[package]] name = "slab" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + [[package]] name = "socket2" version = "0.3.19" @@ -1318,12 +1574,124 @@ dependencies = [ "lock_api", ] +[[package]] +name = "sqlformat" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c" +dependencies = [ + "lazy_static", + "maplit", + "nom", + "regex", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2739d54a2ae9fdd0f545cb4e4b5574efb95e2ec71b7f921678e246fb20dcaaf" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1cad9cae4ca8947eba1a90e8ec7d3c59e7a768e2f120dc9013b669c34a90711" +dependencies = [ + "ahash 0.6.3", + "atoi", + "bitflags", + "byteorder", + "bytes 1.0.1", + "chrono", + "crc", + "crossbeam-channel", + "crossbeam-queue", + "crossbeam-utils", + "either", + "futures-channel", + "futures-core", + "futures-util", + "hashlink", + "hex", + "itoa", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "once_cell", + "parking_lot", + "percent-encoding", + "rustls", + "serde", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "tokio-stream", + "url", + "webpki", + "webpki-roots 0.21.0", + "whoami", +] + +[[package]] +name = "sqlx-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01caee2b3935b4efe152f3262afbe51546ce3b1fc27ad61014e1b3cf5f55366e" +dependencies = [ + "dotenv", + "either", + "futures", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce2e16b6774c671cc183e1d202386fdf9cde1e8468c1894a7f2a63eb671c4f4" +dependencies = [ + "once_cell", + "tokio", + "tokio-rustls", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "syn" version = "1.0.60" @@ -1335,6 +1703,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.2.0" @@ -1414,9 +1788,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.1.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6714d663090b6b0acb0fa85841c6d66233d150cdb2602c8f9b8abb03370beb3f" +checksum = "134af885d758d645f0f0505c9a8b3f9bf8a348fd822e112ab5248138348f1722" dependencies = [ "autocfg", "bytes 1.0.1", @@ -1430,9 +1804,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42517d2975ca3114b22a16192634e8241dc5cc1f130be194645970cc1c371494" +checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" dependencies = [ "proc-macro2", "quote", @@ -1460,6 +1834,17 @@ dependencies = [ "webpki", ] +[[package]] +name = "tokio-stream" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e177a5d8c3bf36de9ebe6d58537d8879e964332f93fb3339e43f618c81361af0" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.6.3" @@ -1482,11 +1867,12 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.22" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" +checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1494,9 +1880,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.11" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada" +checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2" dependencies = [ "proc-macro2", "quote", @@ -1586,12 +1972,24 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + [[package]] name = "unicode-xid" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "untrusted" version = "0.7.1" @@ -1762,6 +2160,16 @@ dependencies = [ "webpki", ] +[[package]] +name = "whoami" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e296f550993cba2c5c3eba5da0fb335562b2fa3d97b7a8ac9dc91f40a3abc70" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1802,6 +2210,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -1872,10 +2286,22 @@ dependencies = [ "serenity", ] +[[package]] +name = "youmubot-db-sql" +version = "0.1.0" +dependencies = [ + "chrono", + "either", + "futures-util", + "sqlx", + "thiserror", +] + [[package]] name = "youmubot-osu" version = "0.1.0" dependencies = [ + "bincode", "bitflags", "chrono", "dashmap", @@ -1887,6 +2313,7 @@ dependencies = [ "serde_json", "serenity", "youmubot-db", + "youmubot-db-sql", "youmubot-prelude", ] @@ -1904,4 +2331,5 @@ dependencies = [ "serenity", "tokio", "youmubot-db", + "youmubot-db-sql", ] diff --git a/Cargo.toml b/Cargo.toml index 7fd1f61..578a71d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "youmubot-prelude", "youmubot-db", + "youmubot-db-sql", "youmubot-core", "youmubot-cf", "youmubot-osu", diff --git a/youmubot-cf/src/announcer.rs b/youmubot-cf/src/announcer.rs index f65d68f..bd088ec 100644 --- a/youmubot-cf/src/announcer.rs +++ b/youmubot-cf/src/announcer.rs @@ -78,7 +78,7 @@ async fn update_user( .await? .into_iter() .next() - .ok_or(Error::msg("Not found"))?; + .ok_or_else(|| Error::msg("Not found"))?; let rating_changes = info.rating_changes(&*client).await?; diff --git a/youmubot-cf/src/embed.rs b/youmubot-cf/src/embed.rs index 3e6f45b..0d23e23 100644 --- a/youmubot-cf/src/embed.rs +++ b/youmubot-cf/src/embed.rs @@ -31,12 +31,12 @@ pub fn user_embed<'a>(user: &User, e: &'a mut CreateEmbed) -> &'a mut CreateEmbe .url(user.profile_url()) .description(format!( "{}\n{}", - if name == "" { + if name.is_empty() { "".to_owned() } else { format!("**{}**", name) }, - if place == "" { + if place.is_empty() { "".to_owned() } else { format!("from **{}**", place) diff --git a/youmubot-cf/src/hook.rs b/youmubot-cf/src/hook.rs index 95946f3..23fc3e9 100644 --- a/youmubot-cf/src/hook.rs +++ b/youmubot-cf/src/hook.rs @@ -77,11 +77,11 @@ impl ContestCache { } async fn get_from_list(&self, contest_id: u64) -> Result { - let last_updated = self.all_list.read().await.1.clone(); + let last_updated = self.all_list.read().await.1; 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()); + let mut v = self.all_list.write().await; + *v = (Contest::list(&*self.http, true).await?, Instant::now()); } self.all_list .read() @@ -194,7 +194,7 @@ fn print_info_message<'a>( problems .as_ref() .map(|v| format!(" | **{}** problems", v.len())) - .unwrap_or("".to_owned()), + .unwrap_or_else(|| "".to_owned()), ) .push( contest @@ -203,7 +203,7 @@ fn print_info_message<'a>( .map(|v| { format!(" | from **{}**", Utc.timestamp(*v as i64, 0).to_rfc2822()) }) - .unwrap_or("".to_owned()), + .unwrap_or_else(|| "".to_owned()), ) .push(format!(" | duration **{}**", duration)); if let Some(p) = &contest.prepared_by { @@ -218,20 +218,21 @@ fn print_info_message<'a>( e.description(m.build()) } +#[allow(clippy::needless_lifetimes)] // Doesn't really work async fn parse_capture<'a>( contest_cache: &ContestCache, cap: Captures<'a>, ) -> Result<(ContestOrProblem, &'a str), CommandError> { let contest_id: u64 = cap .name("contest") - .ok_or(CommandError::from("Contest not captured"))? + .ok_or_else(|| CommandError::from("Contest not captured"))? .as_str() .parse()?; 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"))? { - if &problem.index == p.as_str() { + for problem in problems.ok_or_else(|| CommandError::from("Contest hasn't started"))? { + if problem.index == p.as_str() { return Ok(( ContestOrProblem::Problem(problem), cap.get(0).unwrap().as_str(), diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index 21adae3..5c162cb 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -56,7 +56,7 @@ pub async fn profile(ctx: &Context, m: &Message, mut args: Args) -> CommandResul let data = ctx.data.read().await; let handle = args .single::() - .unwrap_or(UsernameArg::mention(m.author.id)); + .unwrap_or_else(|_| UsernameArg::mention(m.author.id)); let http = data.get::().unwrap(); let handle = match handle { @@ -142,9 +142,7 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult { let everyone = { let db = CfSavedUsers::open(&*data); let db = db.borrow()?; - db.iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect::>() + db.iter().map(|(k, v)| (*k, v.clone())).collect::>() }; let guild = m.guild_id.expect("Guild-only command"); let mut ranks = everyone @@ -216,7 +214,7 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult { format!("#{}", id), cfu.rating .map(|v| v.to_string()) - .unwrap_or("----".to_owned()), + .unwrap_or_else(|| "----".to_owned()), cfu.handle, mem.distinct(), hw = handle_width, @@ -264,7 +262,7 @@ pub async fn contestranks(ctx: &Context, m: &Message, mut args: Args) -> Command .map(|v| v.map(|v| (cf_user.handle, v))) }) .collect::>() - .filter_map(|v| future::ready(v)) + .filter_map(future::ready) .collect::>() .await; let http = data.get::().unwrap(); @@ -394,7 +392,7 @@ pub(crate) async fn contest_rank_table( table.push(" | "); if p.points > 0.0 { table.push(format!("{:^4.0}", p.points)); - } else if let Some(_) = p.best_submission_time_seconds { + } else if p.best_submission_time_seconds.is_some() { table.push(format!("{:^4}", "?")); } else if p.rejected_attempt_count > 0 { table.push(format!("{:^4}", format!("-{}", p.rejected_attempt_count))); diff --git a/youmubot-cf/src/live.rs b/youmubot-cf/src/live.rs index 46c3f64..a589339 100644 --- a/youmubot-cf/src/live.rs +++ b/youmubot-cf/src/live.rs @@ -51,7 +51,7 @@ pub async fn watch_contest( } }) .collect::>() - .filter_map(|v| future::ready(v)) + .filter_map(future::ready) .collect() .await; @@ -323,7 +323,7 @@ enum Change { fn analyze_change(contest: &Contest, old: &ProblemResult, new: &ProblemResult) -> Option { use Change::*; - if old.points == new.points { + if (old.points - new.points).abs() < 0.001 { if new.rejected_attempt_count > old.rejected_attempt_count { if new.result_type == ProblemResultType::Preliminary { return Some(Attempted); @@ -335,25 +335,23 @@ fn analyze_change(contest: &Contest, old: &ProblemResult, new: &ProblemResult) - return Some(Accepted); } None - } else { - if new.points == 0.0 { - if new.result_type == ProblemResultType::Preliminary { - if contest.phase == ContestPhase::Coding { - Some(Hacked) - } else { - None // Just changes to In Queue... - } + } else if new.points == 0.0 { + if new.result_type == ProblemResultType::Preliminary { + if contest.phase == ContestPhase::Coding { + Some(Hacked) } else { - Some(TestFailed) - } - } else if new.points > old.points { - if new.result_type == ProblemResultType::Preliminary { - Some(PretestsPassed) - } else { - Some(Accepted) + None // Just changes to In Queue... } } else { - Some(PretestsPassed) + Some(TestFailed) } + } else if new.points > old.points { + if new.result_type == ProblemResultType::Preliminary { + Some(PretestsPassed) + } else { + Some(Accepted) + } + } else { + Some(PretestsPassed) } } diff --git a/youmubot-core/src/admin/mod.rs b/youmubot-core/src/admin/mod.rs index a8accac..3ab3bf9 100644 --- a/youmubot-core/src/admin/mod.rs +++ b/youmubot-core/src/admin/mod.rs @@ -80,7 +80,7 @@ async fn ban(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { ) .await?; msg.guild_id - .ok_or(Error::msg("Can't get guild from message?"))? // we had a contract + .ok_or_else(|| Error::msg("Can't get guild from message?"))? // we had a contract .ban_with_reason(&ctx.http, user, dmds, &reason) .await?; } @@ -88,7 +88,7 @@ async fn ban(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { 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 + .ok_or_else(|| Error::msg("Can't get guild from message?"))? // we had a contract .ban(&ctx.http, user, dmds) .await?; } diff --git a/youmubot-core/src/admin/soft_ban.rs b/youmubot-core/src/admin/soft_ban.rs index 6350cae..5a5011f 100644 --- a/youmubot-core/src/admin/soft_ban.rs +++ b/youmubot-core/src/admin/soft_ban.rs @@ -28,7 +28,9 @@ pub async fn soft_ban(ctx: &Context, msg: &Message, mut args: Args) -> CommandRe } else { Some(args.single::()?) }; - let guild = msg.guild_id.ok_or(Error::msg("Command is guild only"))?; + let guild = msg + .guild_id + .ok_or_else(|| Error::msg("Command is guild only"))?; let mut db = SoftBans::open(&*data); let val = db @@ -37,7 +39,7 @@ pub async fn soft_ban(ctx: &Context, msg: &Message, mut args: Args) -> CommandRe .map(|v| (v.role, v.periodical_bans.get(&user.id).cloned())); let (role, current_ban_deadline) = match val { None => { - msg.reply(&ctx, format!("โš  This server has not enabled the soft-ban feature. Check out `y!a soft-ban-init`.")).await?; + msg.reply(&ctx, "โš  This server has not enabled the soft-ban feature. Check out `y!a soft-ban-init`.").await?; return Ok(()); } Some(v) => v, @@ -58,7 +60,7 @@ pub async fn soft_ban(ctx: &Context, msg: &Message, mut args: Args) -> CommandRe Some(v) => { // Add the duration into the ban timeout. let until = - current_ban_deadline.unwrap_or(Utc::now()) + chrono::Duration::from_std(v.0)?; + current_ban_deadline.unwrap_or_else(Utc::now) + chrono::Duration::from_std(v.0)?; msg.reply( &ctx, format!("โ›“ Soft-banning user {} until {}.", user.tag(), until), @@ -86,10 +88,7 @@ pub async fn soft_ban_init(ctx: &Context, msg: &Message, mut args: Args) -> Comm let guild = msg.guild(&ctx).await.unwrap(); // Check whether the role_id is the one we wanted if !guild.roles.contains_key(&role_id) { - Err(Error::msg(format!( - "{} is not a role in this server.", - role_id - )))?; + return Err(Error::msg(format!("{} is not a role in this server.", role_id)).into()); } // Check if we already set up let mut db = SoftBans::open(&*data); @@ -100,7 +99,7 @@ pub async fn soft_ban_init(ctx: &Context, msg: &Message, mut args: Args) -> Comm .insert(guild.id, ServerSoftBans::new(role_id)); msg.react(&ctx, '๐Ÿ‘Œ').await?; } else { - Err(Error::msg("Server already set up soft-bans."))? + return Err(Error::msg("Server already set up soft-bans.").into()); } Ok(()) } diff --git a/youmubot-core/src/community/mod.rs b/youmubot-core/src/community/mod.rs index aef503c..77632d0 100644 --- a/youmubot-core/src/community/mod.rs +++ b/youmubot-core/src/community/mod.rs @@ -68,7 +68,7 @@ pub async fn choose(ctx: &Context, m: &Message, mut args: Args) -> CommandResult }) .unwrap_or(false) }) - .map(|mem| future::ready(mem)) + .map(future::ready) .collect::>() .filter_map(|member| async move { // Filter by role if provided @@ -110,7 +110,7 @@ pub async fn choose(ctx: &Context, m: &Message, mut args: Args) -> CommandResult .push(" ") .push( role.map(|r| format!("{}s", r.mention())) - .unwrap_or("potential prayers".to_owned()), + .unwrap_or_else(|| "potential prayers".to_owned()), ) .push(", ") .push(winner.mention()) diff --git a/youmubot-core/src/community/roles.rs b/youmubot-core/src/community/roles.rs index 76be4ce..4b9810c 100644 --- a/youmubot-core/src/community/roles.rs +++ b/youmubot-core/src/community/roles.rs @@ -388,7 +388,7 @@ mod reaction_watcher { .flat_map(|(&guild, rs)| { rs.reaction_messages .iter() - .map(move |(m, r)| (guild, m.clone(), r.clone())) + .map(move |(m, r)| (guild, *m, r.clone())) }) .collect(); Ok(Self { @@ -554,11 +554,11 @@ mod reaction_watcher { let role = Roles::open(&*data) .borrow()? .get(&guild) - .ok_or(Error::msg("guild no longer has role list"))? + .ok_or_else(|| Error::msg("guild no longer has role list"))? .reaction_messages .get(&message) .map(|msg| &msg.roles[..]) - .ok_or(Error::msg("message is no longer a role list handler"))? + .ok_or_else(|| Error::msg("message is no longer a role list handler"))? .iter() .find_map(|(role, role_reaction)| { if &reaction.as_inner_ref().emoji == role_reaction { diff --git a/youmubot-core/src/community/votes.rs b/youmubot-core/src/community/votes.rs index a18154f..913fcfa 100644 --- a/youmubot-core/src/community/votes.rs +++ b/youmubot-core/src/community/votes.rs @@ -87,7 +87,7 @@ pub async fn vote(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult let panel = channel.send_message(&ctx, |c| { c.content("@here").embed(|e| { e.author(|au| { - au.icon_url(author.avatar_url().unwrap_or("".to_owned())) + au.icon_url(author.avatar_url().unwrap_or_else(|| "".to_owned())) .name(&author.name) }) .title(format!("You have {} to vote!", _duration)) @@ -97,7 +97,6 @@ pub async fn vote(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult }) }).await?; msg.delete(&ctx).await?; - drop(msg); // React on all the choices choices @@ -160,7 +159,7 @@ pub async fn vote(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult result.sort_unstable_by(|(_, v), (_, w)| w.len().cmp(&v.len())); - if result.len() == 0 { + if result.is_empty() { msg.reply( &ctx, MessageBuilder::new() @@ -227,7 +226,7 @@ fn pick_n_reactions(n: usize) -> Result, Error> { const MAX_CHOICES: usize = 15; // All the defined reactions. -const REACTIONS: [&'static str; 90] = [ +const REACTIONS: [&str; 90] = [ "๐Ÿ˜€", "๐Ÿ˜", "๐Ÿ˜‚", "๐Ÿคฃ", "๐Ÿ˜ƒ", "๐Ÿ˜„", "๐Ÿ˜…", "๐Ÿ˜†", "๐Ÿ˜‰", "๐Ÿ˜Š", "๐Ÿ˜‹", "๐Ÿ˜Ž", "๐Ÿ˜", "๐Ÿ˜˜", "๐Ÿฅฐ", "๐Ÿ˜—", "๐Ÿ˜™", "๐Ÿ˜š", "โ˜บ๏ธ", "๐Ÿ™‚", "๐Ÿค—", "๐Ÿคฉ", "๐Ÿค”", "๐Ÿคจ", "๐Ÿ˜", "๐Ÿ˜‘", "๐Ÿ˜ถ", "๐Ÿ™„", "๐Ÿ˜", "๐Ÿ˜ฃ", "๐Ÿ˜ฅ", "๐Ÿ˜ฎ", "๐Ÿค", "๐Ÿ˜ฏ", "๐Ÿ˜ช", "๐Ÿ˜ซ", "๐Ÿ˜ด", "๐Ÿ˜Œ", "๐Ÿ˜›", "๐Ÿ˜œ", "๐Ÿ˜", "๐Ÿคค", "๐Ÿ˜’", "๐Ÿ˜“", "๐Ÿ˜”", "๐Ÿ˜•", "๐Ÿ™ƒ", "๐Ÿค‘", diff --git a/youmubot-core/src/fun/mod.rs b/youmubot-core/src/fun/mod.rs index 4a27b0b..fbe5556 100644 --- a/youmubot-core/src/fun/mod.rs +++ b/youmubot-core/src/fun/mod.rs @@ -95,7 +95,7 @@ async fn pick(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { .peekable(); // If we have the first argument as question, use it. let question = match choices.peek() { - Some(ref q) if q.starts_with("?") => Some(q.replacen("?", "", 1) + "?"), + Some(ref q) if q.starts_with('?') => Some(q.replacen("?", "", 1) + "?"), _ => None, }; // If we have a question, that's not a choice. diff --git a/youmubot-core/src/fun/names.rs b/youmubot-core/src/fun/names.rs index ede42dd..9736e10 100644 --- a/youmubot-core/src/fun/names.rs +++ b/youmubot-core/src/fun/names.rs @@ -10,7 +10,7 @@ pub fn name_from_userid(u: UserId) -> (&'static str, &'static str) { ) } -const FIRST_NAMES: [&'static str; 440] = [ +const FIRST_NAMES: [&str; 440] = [ // A Female names "Ai", "Aiko", @@ -473,7 +473,7 @@ const FIRST_NAMES: [&'static str; 440] = [ "Yusuke", ]; -const LAST_NAMES: [&'static str; 1051] = [ +const LAST_NAMES: [&str; 1051] = [ // A Surnames "Abe", "Abukara", diff --git a/youmubot-db-sql/Cargo.toml b/youmubot-db-sql/Cargo.toml new file mode 100644 index 0000000..e8f3f71 --- /dev/null +++ b/youmubot-db-sql/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "youmubot-db-sql" +version = "0.1.0" +authors = ["Natsu Kagami "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "chrono", "offline"] } +thiserror = "1" +chrono = "0.4" +futures-util = "0.3" +either = "1" diff --git a/youmubot-db-sql/migrations/20210329111010_osu_users.sql b/youmubot-db-sql/migrations/20210329111010_osu_users.sql new file mode 100644 index 0000000..937032b --- /dev/null +++ b/youmubot-db-sql/migrations/20210329111010_osu_users.sql @@ -0,0 +1,12 @@ +-- Add migration script here + +CREATE TABLE osu_users ( + user_id BIGINT NOT NULL PRIMARY KEY, + id BIGINT NOT NULL UNIQUE, + last_update DATETIME NOT NULL, + pp_std REAL NULL, + pp_taiko REAL NULL, + pp_mania REAL NULL, + pp_catch REAL NULL, + failures INT NOT NULL DEFAULT 0 +); diff --git a/youmubot-db-sql/migrations/20210331094454_more_osu.sql b/youmubot-db-sql/migrations/20210331094454_more_osu.sql new file mode 100644 index 0000000..b58a6ab --- /dev/null +++ b/youmubot-db-sql/migrations/20210331094454_more_osu.sql @@ -0,0 +1,45 @@ +-- Add migration script here + +CREATE TABLE osu_last_beatmaps ( + channel_id BIGINT NOT NULL PRIMARY KEY, + beatmap BLOB NOT NULL, + mode INT NOT NULL +); + +CREATE TABLE osu_user_best_scores ( + beatmap_id BIGINT NOT NULL, + mode INT NOT NULL, + user_id INT NOT NULL REFERENCES osu_users(user_id) ON DELETE CASCADE, + mods BIGINT NOT NULL, + + cached_at DATETIME NOT NULL, + score BLOB NOT NULL, + + PRIMARY KEY (beatmap_id, mode, user_id, mods) +); + +CREATE TABLE osu_cached_beatmaps ( + beatmap_id BIGINT NOT NULL, + mode INT NOT NULL, + + cached_at DATETIME NOT NULL, + beatmap BLOB NOT NULL, + + PRIMARY KEY (beatmap_id, mode) +); + +CREATE TABLE osu_cached_beatmapsets ( + beatmapset_id BIGINT NOT NULL, + beatmap_id BIGINT NOT NULL, + mode INT NOT NULL, + + PRIMARY KEY (beatmapset_id, beatmap_id, mode), + FOREIGN KEY (beatmap_id, mode) REFERENCES osu_cached_beatmaps (beatmap_id, mode) +); + +CREATE TABLE osu_cached_beatmap_contents ( + beatmap_id BIGINT NOT NULL PRIMARY KEY, + + cached_at DATETIME NOT NULL, + content BLOB NOT NULL +); diff --git a/youmubot-db-sql/sqlx-data.json b/youmubot-db-sql/sqlx-data.json new file mode 100644 index 0000000..f30f431 --- /dev/null +++ b/youmubot-db-sql/sqlx-data.json @@ -0,0 +1,491 @@ +{ + "db": "SQLite", + "1bf34dddbe994d6124c9382c75e70e1347329e945de2eefad4bfcab5f81b73ce": { + "query": "SELECT\n channel_id as \"channel_id: i64\",\n beatmap,\n mode as \"mode: u8\"\n FROM osu_last_beatmaps\n WHERE channel_id = ?", + "describe": { + "columns": [ + { + "name": "channel_id: i64", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "beatmap", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "mode: u8", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + } + }, + "235312a1aad1a58c2f7f2d817945bbac57c38ad2c51c1924683d13d045f21ad9": { + "query": "SELECT\n beatmap_id as \"beatmap_id: i64\",\n mode as \"mode: u8\",\n user_id as \"user_id: i64\",\n mods as \"mods: i64\",\n cached_at as \"cached_at: DateTime\",\n score as \"score: Vec\"\n FROM osu_user_best_scores\n WHERE\n beatmap_id = ?\n AND mode = ?\n AND user_id = ?", + "describe": { + "columns": [ + { + "name": "beatmap_id: i64", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "mode: u8", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "user_id: i64", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "mods: i64", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "cached_at: DateTime", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "score: Vec", + "ordinal": 5, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + } + }, + "25077e7b2657eb918fa49acc16ceba14a004ed503c174073a1db184d902ee393": { + "query": "\n INSERT INTO\n osu_user_best_scores (beatmap_id, mode, user_id, mods, cached_at, score)\n VALUES\n (?, ?, ?, ?, ?, ?)\n ON CONFLICT (beatmap_id, mode, user_id, mods)\n DO UPDATE\n SET\n cached_at = excluded.cached_at,\n score = excluded.score\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + } + }, + "27edec2f76b1bc48e87b66e6d27e6784e0b0c17dec013feb05c4b7291b8b4a5f": { + "query": "SELECT\n user_id as \"user_id: i64\",\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\"\n FROM osu_users WHERE id = ?", + "describe": { + "columns": [ + { + "name": "user_id: i64", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "id: i64", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "last_update: DateTime", + "ordinal": 2, + "type_info": "Datetime" + }, + { + "name": "pp_std", + "ordinal": 3, + "type_info": "Float" + }, + { + "name": "pp_taiko", + "ordinal": 4, + "type_info": "Float" + }, + { + "name": "pp_mania", + "ordinal": 5, + "type_info": "Float" + }, + { + "name": "pp_catch", + "ordinal": 6, + "type_info": "Float" + }, + { + "name": "failures: u8", + "ordinal": 7, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + true, + true, + true, + false + ] + } + }, + "296c95c7ead4d747a4da007b4b6e28d3c6c1c4bb654c82cc40bf61390c3dad4b": { + "query": "SELECT\n beatmap_id as \"beatmap_id: i64\",\n cached_at as \"cached_at: DateTime\",\n content as \"content: Vec\"\n FROM osu_cached_beatmap_contents\n WHERE\n beatmap_id = ? ", + "describe": { + "columns": [ + { + "name": "beatmap_id: i64", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "cached_at: DateTime", + "ordinal": 1, + "type_info": "Datetime" + }, + { + "name": "content: Vec", + "ordinal": 2, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + } + }, + "3c9d163aa2b752afc74e7b0909a9c1995cd019d9798a992eddc2e778f36f2d4c": { + "query": "SELECT\n user_id as \"user_id: i64\",\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\"\n FROM osu_users", + "describe": { + "columns": [ + { + "name": "user_id: i64", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "id: i64", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "last_update: DateTime", + "ordinal": 2, + "type_info": "Datetime" + }, + { + "name": "pp_std", + "ordinal": 3, + "type_info": "Float" + }, + { + "name": "pp_taiko", + "ordinal": 4, + "type_info": "Float" + }, + { + "name": "pp_mania", + "ordinal": 5, + "type_info": "Float" + }, + { + "name": "pp_catch", + "ordinal": 6, + "type_info": "Float" + }, + { + "name": "failures: u8", + "ordinal": 7, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + true, + true, + true, + true, + false + ] + } + }, + "4b033607229deba540f80e469753e2125b6d8134346144f462325dc025221044": { + "query": "SELECT\n beatmap.beatmap_id as \"beatmap_id: i64\",\n beatmap.mode as \"mode: u8\",\n beatmap.cached_at as \"cached_at: DateTime\",\n beatmap.beatmap as \"beatmap: Vec\"\n FROM osu_cached_beatmapsets\n INNER JOIN osu_cached_beatmaps AS beatmap\n ON osu_cached_beatmapsets.beatmap_id = beatmap.beatmap_id\n AND osu_cached_beatmapsets.mode = beatmap.mode\n WHERE\n beatmapset_id = ?\n ", + "describe": { + "columns": [ + { + "name": "beatmap_id: i64", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "mode: u8", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "cached_at: DateTime", + "ordinal": 2, + "type_info": "Datetime" + }, + { + "name": "beatmap: Vec", + "ordinal": 3, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false + ] + } + }, + "5210e3e5610bb968b0b11411b99956d1bf358f2c1e35c4eb5730388ce0c2fe09": { + "query": "INSERT INTO\n osu_last_beatmaps (channel_id, beatmap, mode)\n VALUES\n (?, ?, ?)\n ON CONFLICT (channel_id) DO UPDATE\n SET\n beatmap = excluded.beatmap,\n mode = excluded.mode", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + } + }, + "55fc5c2846680c32a2c9646e45cc578cff25cde57cca19f5fb53dceacc154954": { + "query": "SELECT\n user_id as \"user_id: i64\",\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\"\n FROM osu_users WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "user_id: i64", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "id: i64", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "last_update: DateTime", + "ordinal": 2, + "type_info": "Datetime" + }, + { + "name": "pp_std", + "ordinal": 3, + "type_info": "Float" + }, + { + "name": "pp_taiko", + "ordinal": 4, + "type_info": "Float" + }, + { + "name": "pp_mania", + "ordinal": 5, + "type_info": "Float" + }, + { + "name": "pp_catch", + "ordinal": 6, + "type_info": "Float" + }, + { + "name": "failures: u8", + "ordinal": 7, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + true, + true, + true, + false + ] + } + }, + "6125c1c187029c7ac6e1e9519445e49942ddf6068a16f000dd0750ab8a9d52c2": { + "query": "\n INSERT INTO\n osu_cached_beatmaps (beatmap_id, mode, cached_at, beatmap)\n VALUES\n (?, ?, ?, ?)\n ON CONFLICT (beatmap_id, mode)\n DO UPDATE\n SET\n cached_at = excluded.cached_at,\n beatmap = excluded.beatmap\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + } + }, + "6bfd02cb36c9b74ed4c69eb694e936ba2ee8d3864e2a43b43db78afc32a47384": { + "query": "\n INSERT INTO\n osu_cached_beatmap_contents (beatmap_id, cached_at, content)\n VALUES\n (?, ?, ?)\n ON CONFLICT (beatmap_id)\n DO UPDATE\n SET\n cached_at = excluded.cached_at,\n content = excluded.content\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + } + }, + "6c95dc522e1e8490358ce4c5fea08fe50300ab4092b33eef44aba85f4a43c818": { + "query": "INSERT\n INTO osu_users(user_id, id, last_update, pp_std, pp_taiko, pp_mania, pp_catch, failures)\n VALUES(?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (user_id) WHERE id = ? DO UPDATE\n SET\n last_update = excluded.last_update,\n pp_std = excluded.pp_std,\n pp_taiko = excluded.pp_taiko,\n pp_mania = excluded.pp_mania,\n pp_catch = excluded.pp_catch,\n failures = excluded.failures\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 9 + }, + "nullable": [] + } + }, + "8b9ad43442b7fa520f2eae498d2ee08264810e49c28bd8ddffaa9f444cada1b5": { + "query": "INSERT INTO osu_cached_beatmapsets(beatmapset_id, beatmap_id, mode)\n VALUES (?, ?, ?)\n ON CONFLICT DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + } + }, + "95541f737a8dfc7f440840617bed87ebde6dabdd70e2ba7b110ebec91e7feda7": { + "query": "SELECT\n beatmap_id as \"beatmap_id: i64\",\n mode as \"mode: u8\",\n user_id as \"user_id: i64\",\n mods as \"mods: i64\",\n cached_at as \"cached_at: DateTime\",\n score as \"score: Vec\"\n FROM osu_user_best_scores\n WHERE\n beatmap_id = ?\n AND mode = ?", + "describe": { + "columns": [ + { + "name": "beatmap_id: i64", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "mode: u8", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "user_id: i64", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "mods: i64", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "cached_at: DateTime", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "score: Vec", + "ordinal": 5, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + } + }, + "c83421661626cffd81d5590035ae5283a5b0e8a03696ae479b3d275b81b8af83": { + "query": "DELETE FROM osu_user_best_scores WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + } + }, + "d428568e88b653317cbe2c5336e6cdee0862df09faaa6c1fa09869d79438e427": { + "query": "DELETE FROM osu_users WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + } + }, + "d7c91077f904543740a12185fac7756346aa50a63b911414ee8f7a4a0d6dd1cc": { + "query": "SELECT\n beatmap_id as \"beatmap_id: i64\",\n mode as \"mode: u8\",\n cached_at as \"cached_at: DateTime\",\n beatmap as \"beatmap: Vec\"\n FROM osu_cached_beatmaps\n WHERE\n beatmap_id = ?\n AND mode = ?\n ", + "describe": { + "columns": [ + { + "name": "beatmap_id: i64", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "mode: u8", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "cached_at: DateTime", + "ordinal": 2, + "type_info": "Datetime" + }, + { + "name": "beatmap: Vec", + "ordinal": 3, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false, + false + ] + } + } +} \ No newline at end of file diff --git a/youmubot-db-sql/src/lib.rs b/youmubot-db-sql/src/lib.rs new file mode 100644 index 0000000..0662619 --- /dev/null +++ b/youmubot-db-sql/src/lib.rs @@ -0,0 +1,48 @@ +use sqlx::sqlite; +use std::path::Path; + +pub use errors::*; + +/// The DB constructs that will be used in the package. +pub use sqlite::{SqliteConnection as Connection, SqliteError, SqlitePool as Pool}; +pub use sqlx::Sqlite as Database; + +/// Models defined in the database. +pub mod models; + +/// Create a new pool of sqlite connections to the given database path, +/// run migrations on it and return the result. +pub async fn connect(path: impl AsRef) -> Result { + let pool = Pool::connect_with( + sqlite::SqliteConnectOptions::new() + .filename(path) + .foreign_keys(true) + .create_if_missing(true) + .journal_mode(sqlite::SqliteJournalMode::Wal), + ) + .await?; + + // Run migration before we return. + migration::MIGRATOR.run(&pool).await?; + + Ok(pool) +} + +pub mod errors { + /// Default `Result` type used in this package. + pub type Result = std::result::Result; + /// Possible errors in the package. + #[derive(thiserror::Error, Debug)] + pub enum Error { + #[error("sqlx error: {:?}", .0)] + SQLx(#[from] sqlx::Error), + #[error("sqlx migration error: {:?}", .0)] + Migration(#[from] sqlx::migrate::MigrateError), + } +} + +mod migration { + use sqlx::migrate::Migrator; + + pub(crate) static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); +} diff --git a/youmubot-db-sql/src/models/mod.rs b/youmubot-db-sql/src/models/mod.rs new file mode 100644 index 0000000..b71839b --- /dev/null +++ b/youmubot-db-sql/src/models/mod.rs @@ -0,0 +1,22 @@ +use crate::*; +use futures_util::stream::{Stream, StreamExt}; +use sqlx::{query, query_as, Executor}; + +/// The DateTime used in the package. +pub type DateTime = chrono::DateTime; + +pub mod osu; +pub mod osu_user; + +/// Map a `fetch_many` result to a normal result. +pub(crate) async fn map_many_result( + item: Result, E>, +) -> Option> +where + E: Into, +{ + match item { + Ok(v) => v.right().map(Ok), + Err(e) => Some(Err(e.into())), + } +} diff --git a/youmubot-db-sql/src/models/osu.rs b/youmubot-db-sql/src/models/osu.rs new file mode 100644 index 0000000..9397966 --- /dev/null +++ b/youmubot-db-sql/src/models/osu.rs @@ -0,0 +1,319 @@ +use crate::models::*; + +pub struct LastBeatmap { + pub channel_id: i64, + pub beatmap: Vec, + pub mode: u8, +} + +impl LastBeatmap { + /// Get a [`LastBeatmap`] by the channel id. + pub async fn by_channel_id( + id: i64, + conn: impl Executor<'_, Database = Database>, + ) -> Result> { + let m = query_as!( + LastBeatmap, + r#"SELECT + channel_id as "channel_id: i64", + beatmap, + mode as "mode: u8" + FROM osu_last_beatmaps + WHERE channel_id = ?"#, + id + ) + .fetch_optional(conn) + .await?; + Ok(m) + } +} + +impl LastBeatmap { + /// Store the value. + pub async fn store(&self, conn: impl Executor<'_, Database = Database>) -> Result<()> { + query!( + r#"INSERT INTO + osu_last_beatmaps (channel_id, beatmap, mode) + VALUES + (?, ?, ?) + ON CONFLICT (channel_id) DO UPDATE + SET + beatmap = excluded.beatmap, + mode = excluded.mode"#, + self.channel_id, + self.beatmap, + self.mode, + ) + .execute(conn) + .await?; + Ok(()) + } +} + +pub struct UserBestScore { + pub beatmap_id: i64, + pub mode: u8, + pub user_id: i64, + pub mods: i64, + + pub cached_at: DateTime, + /// To be deserialized by `bincode` + pub score: Vec, +} + +impl UserBestScore { + /// Get a list of scores by the given map and user. + pub async fn by_map_and_user( + beatmap: i64, + mode: u8, + user: i64, + conn: impl Executor<'_, Database = Database>, + ) -> Result> { + query_as!( + UserBestScore, + r#"SELECT + beatmap_id as "beatmap_id: i64", + mode as "mode: u8", + user_id as "user_id: i64", + mods as "mods: i64", + cached_at as "cached_at: DateTime", + score as "score: Vec" + FROM osu_user_best_scores + WHERE + beatmap_id = ? + AND mode = ? + AND user_id = ?"#, + beatmap, + mode, + user + ) + .fetch_all(conn) + .await + .map_err(Error::from) + } + /// Get a list of scores by the given map. + pub async fn by_map( + beatmap: i64, + mode: u8, + conn: impl Executor<'_, Database = Database>, + ) -> Result> { + query_as!( + UserBestScore, + r#"SELECT + beatmap_id as "beatmap_id: i64", + mode as "mode: u8", + user_id as "user_id: i64", + mods as "mods: i64", + cached_at as "cached_at: DateTime", + score as "score: Vec" + FROM osu_user_best_scores + WHERE + beatmap_id = ? + AND mode = ?"#, + beatmap, + mode + ) + .fetch_all(conn) + .await + .map_err(Error::from) + } +} + +impl UserBestScore { + pub async fn store(&mut self, conn: impl Executor<'_, Database = Database>) -> Result<()> { + self.cached_at = chrono::Utc::now(); + query!( + r#" + INSERT INTO + osu_user_best_scores (beatmap_id, mode, user_id, mods, cached_at, score) + VALUES + (?, ?, ?, ?, ?, ?) + ON CONFLICT (beatmap_id, mode, user_id, mods) + DO UPDATE + SET + cached_at = excluded.cached_at, + score = excluded.score + "#, + self.beatmap_id, + self.mode, + self.user_id, + self.mods, + self.cached_at, + self.score + ) + .execute(conn) + .await?; + Ok(()) + } + + pub async fn clear_user( + user_id: i64, + conn: impl Executor<'_, Database = Database>, + ) -> Result<()> { + query!( + "DELETE FROM osu_user_best_scores WHERE user_id = ?", + user_id + ) + .execute(conn) + .await?; + Ok(()) + } +} + +pub struct CachedBeatmap { + pub beatmap_id: i64, + pub mode: u8, + pub cached_at: DateTime, + pub beatmap: Vec, +} + +impl CachedBeatmap { + /// Get a cached beatmap by its id. + pub async fn by_id( + id: i64, + mode: u8, + conn: impl Executor<'_, Database = Database>, + ) -> Result> { + query_as!( + Self, + r#"SELECT + beatmap_id as "beatmap_id: i64", + mode as "mode: u8", + cached_at as "cached_at: DateTime", + beatmap as "beatmap: Vec" + FROM osu_cached_beatmaps + WHERE + beatmap_id = ? + AND mode = ? + "#, + id, + mode + ) + .fetch_optional(conn) + .await + .map_err(Error::from) + } + + pub async fn by_beatmapset( + beatmapset: i64, + conn: impl Executor<'_, Database = Database>, + ) -> Result> { + query_as!( + Self, + r#"SELECT + beatmap.beatmap_id as "beatmap_id: i64", + beatmap.mode as "mode: u8", + beatmap.cached_at as "cached_at: DateTime", + beatmap.beatmap as "beatmap: Vec" + FROM osu_cached_beatmapsets + INNER JOIN osu_cached_beatmaps AS beatmap + ON osu_cached_beatmapsets.beatmap_id = beatmap.beatmap_id + AND osu_cached_beatmapsets.mode = beatmap.mode + WHERE + beatmapset_id = ? + "#, + beatmapset + ) + .fetch_all(conn) + .await + .map_err(Error::from) + } +} + +impl CachedBeatmap { + pub async fn store(&mut self, conn: impl Executor<'_, Database = Database>) -> Result<()> { + self.cached_at = chrono::Utc::now(); + query!( + r#" + INSERT INTO + osu_cached_beatmaps (beatmap_id, mode, cached_at, beatmap) + VALUES + (?, ?, ?, ?) + ON CONFLICT (beatmap_id, mode) + DO UPDATE + SET + cached_at = excluded.cached_at, + beatmap = excluded.beatmap + "#, + self.beatmap_id, + self.mode, + self.cached_at, + self.beatmap + ) + .execute(conn) + .await?; + Ok(()) + } + + pub async fn link_beatmapset( + &self, + beatmapset_id: i64, + conn: impl Executor<'_, Database = Database>, + ) -> Result<()> { + query!( + r#"INSERT INTO osu_cached_beatmapsets(beatmapset_id, beatmap_id, mode) + VALUES (?, ?, ?) + ON CONFLICT DO NOTHING"#, + beatmapset_id, + self.beatmap_id, + self.mode, + ) + .execute(conn) + .await?; + Ok(()) + } +} + +pub struct CachedBeatmapContent { + pub beatmap_id: i64, + pub cached_at: DateTime, + pub content: Vec, +} + +impl CachedBeatmapContent { + /// Get a cached beatmap by its id. + pub async fn by_id( + id: i64, + conn: impl Executor<'_, Database = Database>, + ) -> Result> { + query_as!( + Self, + r#"SELECT + beatmap_id as "beatmap_id: i64", + cached_at as "cached_at: DateTime", + content as "content: Vec" + FROM osu_cached_beatmap_contents + WHERE + beatmap_id = ? "#, + id, + ) + .fetch_optional(conn) + .await + .map_err(Error::from) + } +} + +impl CachedBeatmapContent { + pub async fn store(&mut self, conn: impl Executor<'_, Database = Database>) -> Result<()> { + self.cached_at = chrono::Utc::now(); + query!( + r#" + INSERT INTO + osu_cached_beatmap_contents (beatmap_id, cached_at, content) + VALUES + (?, ?, ?) + ON CONFLICT (beatmap_id) + DO UPDATE + SET + cached_at = excluded.cached_at, + content = excluded.content + "#, + self.beatmap_id, + self.cached_at, + self.content + ) + .execute(conn) + .await?; + Ok(()) + } +} diff --git a/youmubot-db-sql/src/models/osu_user.rs b/youmubot-db-sql/src/models/osu_user.rs new file mode 100644 index 0000000..a339a2c --- /dev/null +++ b/youmubot-db-sql/src/models/osu_user.rs @@ -0,0 +1,118 @@ +use super::*; +use sqlx::{query, query_as, Executor}; + +/// An osu user, as represented in the SQL. +#[derive(Debug, Clone)] +pub struct OsuUser { + pub user_id: i64, + pub id: i64, + pub last_update: DateTime, + pub pp_std: Option, + pub pp_taiko: Option, + pub pp_mania: Option, + pub pp_catch: Option, + /// Number of consecutive update failures + pub failures: u8, +} + +impl OsuUser { + /// Query an user by their user id. + pub async fn by_user_id<'a, E>(user_id: i64, conn: &'a mut E) -> Result> + where + &'a mut E: Executor<'a, Database = Database>, + { + let u = query_as!( + Self, + r#"SELECT + user_id as "user_id: i64", + id as "id: i64", + last_update as "last_update: DateTime", + pp_std, pp_taiko, pp_mania, pp_catch, + failures as "failures: u8" + FROM osu_users WHERE user_id = ?"#, + user_id + ) + .fetch_optional(conn) + .await?; + Ok(u) + } + + /// Query an user by their osu id. + pub async fn by_osu_id<'a, E>(osu_id: i64, conn: &'a mut E) -> Result> + where + &'a mut E: Executor<'a, Database = Database>, + { + let u = query_as!( + Self, + r#"SELECT + user_id as "user_id: i64", + id as "id: i64", + last_update as "last_update: DateTime", + pp_std, pp_taiko, pp_mania, pp_catch, + failures as "failures: u8" + FROM osu_users WHERE id = ?"#, + osu_id + ) + .fetch_optional(conn) + .await?; + Ok(u) + } + + /// Query all users. + pub fn all<'a, E>(conn: &'a mut E) -> impl Stream> + 'a + where + &'a mut E: Executor<'a, Database = Database>, + { + query_as!( + Self, + r#"SELECT + user_id as "user_id: i64", + id as "id: i64", + last_update as "last_update: DateTime", + pp_std, pp_taiko, pp_mania, pp_catch, + failures as "failures: u8" + FROM osu_users"#, + ) + .fetch_many(conn) + .filter_map(map_many_result) + } +} + +impl OsuUser { + /// Stores the user. + pub async fn store<'a, E>(&self, conn: &'a mut E) -> Result<()> + where + &'a mut E: Executor<'a, Database = Database>, + { + query!( + r#"INSERT + INTO osu_users(user_id, id, last_update, pp_std, pp_taiko, pp_mania, pp_catch, failures) + VALUES(?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (user_id) WHERE id = ? DO UPDATE + SET + last_update = excluded.last_update, + pp_std = excluded.pp_std, + pp_taiko = excluded.pp_taiko, + pp_mania = excluded.pp_mania, + pp_catch = excluded.pp_catch, + failures = excluded.failures + "#, + self.user_id, + self.id, + self.last_update, + self.pp_std, + self.pp_taiko, + self.pp_mania, + self.pp_catch, + self.failures, + self.user_id).execute(conn).await?; + Ok(()) + } + + pub async fn delete(user_id: i64, conn: impl Executor<'_, Database = Database>) -> Result<()> { + query!("DELETE FROM osu_users WHERE user_id = ?", user_id) + .execute(conn) + .await?; + Ok(()) + } +} diff --git a/youmubot-db/src/lib.rs b/youmubot-db/src/lib.rs index ec512f5..34f09a3 100644 --- a/youmubot-db/src/lib.rs +++ b/youmubot-db/src/lib.rs @@ -25,7 +25,7 @@ where { /// Load the DB from a path. pub fn load_from_path(path: impl AsRef) -> Result, DBError> { - Ok(Database::::load_from_path(path)?) + Database::::load_from_path(path) } /// Insert into a ShareMap. diff --git a/youmubot-osu/Cargo.toml b/youmubot-osu/Cargo.toml index cbb8762..7307aee 100644 --- a/youmubot-osu/Cargo.toml +++ b/youmubot-osu/Cargo.toml @@ -16,8 +16,10 @@ lazy_static = "1" regex = "1" oppai-rs = "0.2" dashmap = "4" +bincode = "1" youmubot-db = { path = "../youmubot-db" } +youmubot-db-sql = { path = "../youmubot-db-sql" } youmubot-prelude = { path = "../youmubot-prelude" } [dev-dependencies] diff --git a/youmubot-osu/src/discord/announcer.rs b/youmubot-osu/src/discord/announcer.rs index e401ff2..4696a13 100644 --- a/youmubot-osu/src/discord/announcer.rs +++ b/youmubot-osu/src/discord/announcer.rs @@ -17,11 +17,11 @@ use serenity::{ }, CacheAndHttp, }; -use std::{collections::HashMap, sync::Arc}; +use std::{convert::TryInto, sync::Arc}; use youmubot_prelude::*; /// osu! announcer's unique announcer key. -pub const ANNOUNCER_KEY: &'static str = "osu"; +pub const ANNOUNCER_KEY: &str = "osu"; /// The announcer struct implementing youmubot_prelude::Announcer pub struct Announcer { @@ -45,11 +45,14 @@ impl youmubot_prelude::Announcer for Announcer { channels: MemberToChannels, ) -> Result<()> { // For each user... - let data = OsuSavedUsers::open(&*d.read().await).borrow()?.clone(); + let data = d.read().await; + let data = data.get::().unwrap(); let now = chrono::Utc::now(); - let data = data + let users = data.all().await?; + users .into_iter() - .map(|(user_id, osu_user)| { + .map(|mut osu_user| { + let user_id = osu_user.user_id; let channels = &channels; let ctx = Context { c: c.clone(), @@ -59,62 +62,33 @@ impl youmubot_prelude::Announcer for Announcer { async move { let channels = channels.channels_of(ctx.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 + return; // 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| { - s.handle_user_mode(&ctx, now, &osu_user, user_id, channels.clone(), *m) - }) - .collect::>() - .try_collect::>() - .await + match std::array::IntoIter::new([ + Mode::Std, + Mode::Taiko, + Mode::Catch, + Mode::Mania, + ]) + .map(|m| s.handle_user_mode(&ctx, now, &osu_user, user_id, channels.clone(), m)) + .collect::>() + .try_collect::>() + .await { - Ok(v) => v, + Ok(v) => { + osu_user.last_update = now; + osu_user.pp = v.try_into().unwrap(); + data.save(osu_user).await.pls_ok(); + } Err(e) => { eprintln!("osu: Cannot update {}: {}", osu_user.id, e); - return ( - user_id, - OsuUser { - failures: Some(osu_user.failures.unwrap_or(0) + 1), - ..osu_user - }, - ); } }; - ( - user_id, - OsuUser { - pp, - last_update: now, - failures: None, - ..osu_user - }, - ) } }) .collect::>() - .collect::>() + .collect::<()>() .await; - // Update users - let db = &*d.read().await; - let mut db = OsuSavedUsers::open(db); - let mut db = db.borrow_mut()?; - data.into_iter() - .for_each(|(k, v)| match db.get(&k).map(|v| v.last_update.clone()) { - Some(d) if d > now => (), - _ => { - if v.failures.unwrap_or(0) > 5 { - eprintln!( - "osu: Removing user {} [{}] due to 5 consecutive failures", - k, v.id - ); - // db.remove(&k); - } else { - db.insert(k, v); - } - } - }); Ok(()) } } @@ -129,9 +103,9 @@ impl Announcer { user_id: UserId, channels: Vec, mode: Mode, - ) -> Result, Error> { + ) -> Result, Error> { let days_since_last_update = (now - osu_user.last_update).num_days() + 1; - let last_update = osu_user.last_update.clone(); + let last_update = osu_user.last_update; let (scores, user) = { let scores = self.scan_user(osu_user, mode).await?; let user = self @@ -141,7 +115,7 @@ impl Announcer { .event_days(days_since_last_update.min(31) as u8) }) .await? - .ok_or(Error::msg("user not found"))?; + .ok_or_else(|| Error::msg("user not found"))?; (scores, user) }; let client = self.client.clone(); @@ -181,7 +155,7 @@ impl Announcer { .await .pls_ok(); }); - Ok(pp) + Ok(pp.map(|v| v as f32)) } async fn scan_user(&self, u: &OsuUser, mode: Mode) -> Result, Error> { @@ -265,20 +239,14 @@ impl<'a> CollectedScore<'a> { async fn send_message(self, ctx: &Context) -> Result> { let (bm, content) = self.get_beatmap(&ctx).await?; self.channels - .into_iter() + .iter() .map(|c| self.send_message_to(*c, ctx, &bm, &content)) .collect::>() .try_collect() .await } - async fn get_beatmap( - &self, - ctx: &Context, - ) -> Result<( - BeatmapWithMode, - impl std::ops::Deref, - )> { + async fn get_beatmap(&self, ctx: &Context) -> Result<(BeatmapWithMode, BeatmapContent)> { let data = ctx.data.read().await; let cache = data.get::().unwrap(); let oppai = data.get::().unwrap(); @@ -314,7 +282,9 @@ impl<'a> CollectedScore<'a> { }) }) .await?; - save_beatmap(&*ctx.data.read().await, channel, &bm).pls_ok(); + save_beatmap(&*ctx.data.read().await, channel, &bm) + .await + .pls_ok(); Ok(m) } } diff --git a/youmubot-osu/src/discord/beatmap_cache.rs b/youmubot-osu/src/discord/beatmap_cache.rs index d41e323..e9de006 100644 --- a/youmubot-osu/src/discord/beatmap_cache.rs +++ b/youmubot-osu/src/discord/beatmap_cache.rs @@ -2,16 +2,15 @@ use crate::{ models::{ApprovalStatus, Beatmap, Mode}, Client, }; -use dashmap::DashMap; use std::sync::Arc; +use youmubot_db_sql::{models::osu as models, Pool}; use youmubot_prelude::*; /// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling. /// Does not cache non-Ranked beatmaps. pub struct BeatmapMetaCache { client: Arc, - cache: DashMap<(u64, Mode), Beatmap>, - beatmapsets: DashMap>, + pool: Pool, } impl TypeMapKey for BeatmapMetaCache { @@ -20,13 +19,20 @@ impl TypeMapKey for BeatmapMetaCache { impl BeatmapMetaCache { /// Create a new beatmap cache. - pub fn new(client: Arc) -> Self { - BeatmapMetaCache { - client, - cache: DashMap::new(), - beatmapsets: DashMap::new(), + pub fn new(client: Arc, pool: Pool) -> Self { + BeatmapMetaCache { client, pool } + } + + #[allow(clippy::wrong_self_convention)] + fn to_cached_beatmap(beatmap: &Beatmap, mode: Option) -> models::CachedBeatmap { + models::CachedBeatmap { + beatmap_id: beatmap.beatmap_id as i64, + mode: mode.unwrap_or(beatmap.mode) as u8, + cached_at: chrono::Utc::now(), + beatmap: bincode::serialize(&beatmap).unwrap(), } } + async fn insert_if_possible(&self, id: u64, mode: Option) -> Result { let beatmap = self .client @@ -37,15 +43,29 @@ impl BeatmapMetaCache { f }) .await - .and_then(|v| v.into_iter().next().ok_or(Error::msg("beatmap not found")))?; + .and_then(|v| { + v.into_iter() + .next() + .ok_or_else(|| Error::msg("beatmap not found")) + })?; if let ApprovalStatus::Ranked(_) = beatmap.approval { - self.cache.insert((id, beatmap.mode), beatmap.clone()); + let mut c = Self::to_cached_beatmap(&beatmap, mode); + c.store(&self.pool).await.pls_ok(); }; Ok(beatmap) } + + async fn get_beatmap_db(&self, id: u64, mode: Mode) -> Result> { + Ok( + models::CachedBeatmap::by_id(id as i64, mode as u8, &self.pool) + .await? + .map(|v| bincode::deserialize(&v.beatmap[..]).unwrap()), + ) + } + /// Get the given beatmap pub async fn get_beatmap(&self, id: u64, mode: Mode) -> Result { - match self.cache.get(&(id, mode)).map(|v| v.clone()) { + match self.get_beatmap_db(id, mode).await? { Some(v) => Ok(v), None => self.insert_if_possible(id, Some(mode)).await, } @@ -53,51 +73,45 @@ impl BeatmapMetaCache { /// Get a beatmap without a mode... pub async fn get_beatmap_default(&self, id: u64) -> Result { - Ok( - match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]) - .iter() - .find_map(|&mode| { - self.cache - .get(&(id, mode)) - .filter(|b| b.mode == mode) - .map(|b| b.clone()) - }) { - Some(v) => v, - None => self.insert_if_possible(id, None).await?, - }, - ) + for mode in std::array::IntoIter::new([Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]) { + if let Ok(Some(bm)) = self.get_beatmap_db(id, mode).await { + if bm.mode == mode { + return Ok(bm); + } + } + } + + self.insert_if_possible(id, None).await } /// Get a beatmapset from its ID. pub async fn get_beatmapset(&self, id: u64) -> Result> { - match self.beatmapsets.get(&id).map(|v| v.clone()) { - Some(v) => { - v.into_iter() - .map(|id| self.get_beatmap_default(id)) - .collect::>() - .try_collect() - .await - } - None => { - let mut beatmaps = self - .client - .beatmaps(crate::BeatmapRequestKind::Beatmapset(id), |f| f) - .await?; - if beatmaps.is_empty() { - return Err(Error::msg("beatmapset not found")); - } - beatmaps.sort_by_key(|b| (b.mode as u8, (b.difficulty.stars * 1000.0) as u64)); // Cast so that Ord is maintained - if let ApprovalStatus::Ranked(_) = &beatmaps[0].approval { - // Save each beatmap. - beatmaps.iter().for_each(|b| { - self.cache.insert((b.beatmap_id, b.mode), b.clone()); - }); - // Save the beatmapset mapping. - self.beatmapsets - .insert(id, beatmaps.iter().map(|v| v.beatmap_id).collect()); - } - Ok(beatmaps) - } + let bms = models::CachedBeatmap::by_beatmapset(id as i64, &self.pool).await?; + if !bms.is_empty() { + return Ok(bms + .into_iter() + .map(|v| bincode::deserialize(&v.beatmap[..]).unwrap()) + .collect()); } + let mut beatmaps = self + .client + .beatmaps(crate::BeatmapRequestKind::Beatmapset(id), |f| f) + .await?; + if beatmaps.is_empty() { + return Err(Error::msg("beatmapset not found")); + } + beatmaps.sort_by_key(|b| (b.mode as u8, (b.difficulty.stars * 1000.0) as u64)); // Cast so that Ord is maintained + if let ApprovalStatus::Ranked(_) = &beatmaps[0].approval { + // Save each beatmap. + let mut t = self.pool.begin().await?; + for b in &beatmaps { + let mut b = Self::to_cached_beatmap(&b, None); + b.store(&mut t).await?; + // Save the beatmapset mapping. + b.link_beatmapset(id as i64, &mut t).await?; + } + t.commit().await?; + } + Ok(beatmaps) } } diff --git a/youmubot-osu/src/discord/cache.rs b/youmubot-osu/src/discord/cache.rs index 37bcab4..2778eca 100644 --- a/youmubot-osu/src/discord/cache.rs +++ b/youmubot-osu/src/discord/cache.rs @@ -4,28 +4,27 @@ use serenity::model::id::ChannelId; use youmubot_prelude::*; /// Save the beatmap into the server data storage. -pub(crate) fn save_beatmap( +pub(crate) async fn save_beatmap( data: &TypeMap, channel_id: ChannelId, bm: &BeatmapWithMode, ) -> Result<()> { - OsuLastBeatmap::open(data) - .borrow_mut()? - .insert(channel_id, (bm.0.clone(), bm.mode())); + data.get::() + .unwrap() + .save(channel_id, &bm.0, bm.1) + .await?; Ok(()) } /// Get the last beatmap requested from this channel. -pub(crate) fn get_beatmap( +pub(crate) async fn get_beatmap( data: &TypeMap, channel_id: ChannelId, ) -> Result> { - let db = OsuLastBeatmap::open(data); - let db = db.borrow()?; - - Ok(db - .get(&channel_id) - .cloned() - .map(|(a, b)| BeatmapWithMode(a, b))) + data.get::() + .unwrap() + .by_channel(channel_id) + .await + .map(|v| v.map(|(bm, mode)| BeatmapWithMode(bm, mode))) } diff --git a/youmubot-osu/src/discord/db.rs b/youmubot-osu/src/discord/db.rs index a52ef0b..0f3eb36 100644 --- a/youmubot-osu/src/discord/db.rs +++ b/youmubot-osu/src/discord/db.rs @@ -1,28 +1,217 @@ use chrono::{DateTime, Utc}; +use youmubot_db_sql::{models::osu as models, models::osu_user as model, Pool}; use crate::models::{Beatmap, Mode, Score}; use serde::{Deserialize, Serialize}; use serenity::model::id::{ChannelId, UserId}; -use std::collections::HashMap; -use youmubot_db::DB; +use youmubot_prelude::*; /// Save the user IDs. -pub type OsuSavedUsers = DB>; +pub struct OsuSavedUsers { + pool: Pool, +} + +impl TypeMapKey for OsuSavedUsers { + type Value = OsuSavedUsers; +} + +impl OsuSavedUsers { + /// Create a new database wrapper. + pub fn new(pool: Pool) -> Self { + Self { pool } + } +} + +impl OsuSavedUsers { + /// Get all users + pub async fn all(&self) -> Result> { + let mut conn = self.pool.acquire().await?; + model::OsuUser::all(&mut conn) + .map(|v| v.map(OsuUser::from).map_err(Error::from)) + .try_collect() + .await + } + + /// Get an user by their user_id. + pub async fn by_user_id(&self, user_id: UserId) -> Result> { + let mut conn = self.pool.acquire().await?; + let u = model::OsuUser::by_user_id(user_id.0 as i64, &mut conn) + .await? + .map(OsuUser::from); + Ok(u) + } + + /// Save the given user. + pub async fn save(&self, u: OsuUser) -> Result<()> { + let mut conn = self.pool.acquire().await?; + Ok(model::OsuUser::from(u).store(&mut conn).await?) + } + + /// Save the given user as a completely new user. + pub async fn new_user(&self, u: OsuUser) -> Result<()> { + let mut t = self.pool.begin().await?; + model::OsuUser::delete(u.user_id.0 as i64, &mut t).await?; + model::OsuUser::from(u).store(&mut t).await?; + t.commit().await?; + Ok(()) + } +} /// Save each channel's last requested beatmap. -pub type OsuLastBeatmap = DB>; +pub struct OsuLastBeatmap(Pool); -/// Save each beatmap's plays by user. -pub type OsuUserBests = - DB>>>; +impl TypeMapKey for OsuLastBeatmap { + type Value = OsuLastBeatmap; +} + +impl OsuLastBeatmap { + pub fn new(pool: Pool) -> Self { + Self(pool) + } +} + +impl OsuLastBeatmap { + pub async fn by_channel(&self, id: impl Into) -> Result> { + let last_beatmap = models::LastBeatmap::by_channel_id(id.into().0 as i64, &self.0).await?; + Ok(match last_beatmap { + Some(lb) => Some((bincode::deserialize(&lb.beatmap[..])?, lb.mode.into())), + None => None, + }) + } + + pub async fn save( + &self, + channel: impl Into, + beatmap: &Beatmap, + mode: Mode, + ) -> Result<()> { + let b = models::LastBeatmap { + channel_id: channel.into().0 as i64, + beatmap: bincode::serialize(beatmap)?, + mode: mode as u8, + }; + b.store(&self.0).await?; + Ok(()) + } +} + +/// Save each channel's last requested beatmap. +pub struct OsuUserBests(Pool); + +impl TypeMapKey for OsuUserBests { + type Value = OsuUserBests; +} + +impl OsuUserBests { + pub fn new(pool: Pool) -> Self { + Self(pool) + } +} + +impl OsuUserBests { + pub async fn by_beatmap(&self, beatmap_id: u64, mode: Mode) -> Result> { + let scores = models::UserBestScore::by_map(beatmap_id as i64, mode as u8, &self.0).await?; + Ok(scores + .into_iter() + .map(|us| { + ( + UserId(us.user_id as u64), + bincode::deserialize(&us.score[..]).unwrap(), + ) + }) + .collect()) + } + + pub async fn save( + &self, + user: impl Into, + mode: Mode, + scores: impl IntoIterator, + ) -> Result<()> { + let user = user.into(); + scores + .into_iter() + .map(|score| models::UserBestScore { + user_id: user.0 as i64, + beatmap_id: score.beatmap_id as i64, + mode: mode as u8, + mods: score.mods.bits() as i64, + cached_at: Utc::now(), + score: bincode::serialize(&score).unwrap(), + }) + .map(|mut us| async move { us.store(&self.0).await }) + .collect::>() + .try_collect::<()>() + .await?; + Ok(()) + } +} /// An osu! saved user. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct OsuUser { + pub user_id: UserId, pub id: u64, pub last_update: DateTime, - #[serde(default)] - pub pp: Vec>, + pub pp: [Option; 4], /// More than 5 failures => gone - pub failures: Option, + pub failures: u8, +} + +impl From for model::OsuUser { + fn from(u: OsuUser) -> Self { + Self { + user_id: u.user_id.0 as i64, + id: u.id as i64, + last_update: u.last_update, + pp_std: u.pp[Mode::Std as usize], + pp_taiko: u.pp[Mode::Taiko as usize], + pp_mania: u.pp[Mode::Mania as usize], + pp_catch: u.pp[Mode::Catch as usize], + failures: u.failures, + } + } +} + +impl From for OsuUser { + fn from(u: model::OsuUser) -> Self { + Self { + user_id: UserId(u.user_id as u64), + id: u.id as u64, + last_update: u.last_update, + pp: [u.pp_std, u.pp_taiko, u.pp_mania, u.pp_catch], + failures: u.failures, + } + } +} + +#[allow(dead_code)] +mod legacy { + use chrono::{DateTime, Utc}; + + use crate::models::{Beatmap, Mode, Score}; + use serde::{Deserialize, Serialize}; + use serenity::model::id::{ChannelId, UserId}; + use std::collections::HashMap; + use youmubot_db::DB; + + pub type OsuSavedUsers = DB>; + + /// An osu! saved user. + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct OsuUser { + pub id: u64, + pub last_update: DateTime, + #[serde(default)] + pub pp: Vec>, + /// More than 5 failures => gone + pub failures: Option, + } + + /// Save each channel's last requested beatmap. + pub type OsuLastBeatmap = DB>; + + /// Save each beatmap's plays by user. + pub type OsuUserBests = + DB>>>; } diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index 93c8963..cf02cec 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -93,10 +93,10 @@ mod beatmapset { let map = &self.maps[page]; let info = match &self.infos[page] { - Some(info) => info.clone(), + Some(info) => *info, None => { let info = self.get_beatmap_info(ctx, map).await; - self.infos[page] = Some(info.clone()); + self.infos[page] = Some(info); info } }; @@ -125,7 +125,8 @@ mod beatmapset { m.channel_id, &BeatmapWithMode(map.clone(), self.mode.unwrap_or(map.mode)), ) - .ok(); + .await + .pls_ok(); Ok(true) } diff --git a/youmubot-osu/src/discord/embeds.rs b/youmubot-osu/src/discord/embeds.rs index cc954a0..3fbada3 100644 --- a/youmubot-osu/src/discord/embeds.rs +++ b/youmubot-osu/src/discord/embeds.rs @@ -199,6 +199,7 @@ pub(crate) fn score_embed<'a>( } impl<'a> ScoreEmbedBuilder<'a> { + #[allow(clippy::many_single_char_names)] pub fn build<'b>(&self, m: &'b mut CreateEmbed) -> &'b mut CreateEmbed { let mode = self.bm.mode(); let b = &self.bm.0; @@ -213,8 +214,8 @@ impl<'a> ScoreEmbedBuilder<'a> { .as_ref() .map(|info| info.stars as f64) .unwrap_or(b.difficulty.stars); - let score_line = match &s.rank { - Rank::SS | Rank::SSH => format!("SS"), + let score_line = match s.rank { + Rank::SS | Rank::SSH => "SS".to_string(), _ if s.perfect => format!("{:.2}% FC", accuracy), Rank::F => { let display = info @@ -224,7 +225,7 @@ impl<'a> ScoreEmbedBuilder<'a> { * 100.0 }) .map(|p| format!("FAILED @ {:.2}%", p)) - .unwrap_or("FAILED".to_owned()); + .unwrap_or_else(|| "FAILED".to_owned()); format!("{:.2}% {} combo [{}]", accuracy, s.max_combo, display) } v => format!( @@ -267,7 +268,7 @@ impl<'a> ScoreEmbedBuilder<'a> { pp.as_ref() .map(|(_, original)| format!("{} ({:.2}pp if FC?)", original, value)) }) - .or(pp.map(|v| v.1)) + .or_else(|| pp.map(|v| v.1)) } else { pp.map(|v| v.1) }; @@ -295,11 +296,11 @@ impl<'a> ScoreEmbedBuilder<'a> { let top_record = self .top_record .map(|v| format!("| #{} top record!", v)) - .unwrap_or("".to_owned()); + .unwrap_or_else(|| "".to_owned()); let world_record = self .world_record .map(|v| format!("| #{} on Global Rankings!", v)) - .unwrap_or("".to_owned()); + .unwrap_or_else(|| "".to_owned()); let diff = b.difficulty.apply_mods(s.mods, Some(stars)); let creator = if b.difficulty_name.contains("'s") { "".to_owned() @@ -364,11 +365,11 @@ impl<'a> ScoreEmbedBuilder<'a> { } } -pub(crate) fn user_embed<'a>( +pub(crate) fn user_embed( u: User, best: Option<(Score, BeatmapWithMode, Option)>, - m: &'a mut CreateEmbed, -) -> &'a mut CreateEmbed { + m: &mut CreateEmbed, +) -> &mut CreateEmbed { m.title(u.username) .url(format!("https://osu.ppy.sh/users/{}", u.id)) .color(0xffb6c1) @@ -377,7 +378,7 @@ pub(crate) fn user_embed<'a>( .field( "Performance Points", u.pp.map(|v| format!("{:.2}pp", v)) - .unwrap_or("Inactive".to_owned()), + .unwrap_or_else(|| "Inactive".to_owned()), false, ) .field("World Rank", format!("#{}", grouped_number(u.rank)), true) @@ -436,7 +437,7 @@ pub(crate) fn user_embed<'a>( Duration( (Utc::now() - v.date) .to_std() - .unwrap_or(std::time::Duration::from_secs(1)) + .unwrap_or_else(|_| std::time::Duration::from_secs(1)) ) )) .push("on ") diff --git a/youmubot-osu/src/discord/hook.rs b/youmubot-osu/src/discord/hook.rs index d5429a9..89ce2c6 100644 --- a/youmubot-osu/src/discord/hook.rs +++ b/youmubot-osu/src/discord/hook.rs @@ -50,6 +50,7 @@ pub fn hook<'a>( msg.channel_id, &bm, ) + .await .pls_ok(); } EmbedType::Beatmapset(b) => { diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 239965f..2e3cf7d 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -52,14 +52,15 @@ impl TypeMapKey for OsuClient { /// - Hooks. Hooks are completely opt-in. /// pub fn setup( - path: &std::path::Path, + _path: &std::path::Path, data: &mut TypeMap, announcers: &mut AnnouncerHandler, ) -> CommandResult { + let sql_client = data.get::().unwrap().clone(); // Databases - OsuSavedUsers::insert_into(&mut *data, &path.join("osu_saved_users.yaml"))?; - OsuLastBeatmap::insert_into(&mut *data, &path.join("last_beatmaps.yaml"))?; - OsuUserBests::insert_into(&mut *data, &path.join("osu_user_bests.yaml"))?; + data.insert::(OsuSavedUsers::new(sql_client.clone())); + data.insert::(OsuLastBeatmap::new(sql_client.clone())); + data.insert::(OsuUserBests::new(sql_client.clone())); // Locks data.insert::( @@ -75,9 +76,12 @@ pub fn setup( }; let osu_client = Arc::new(make_client()); data.insert::(osu_client.clone()); - data.insert::(oppai_cache::BeatmapCache::new(http_client)); + data.insert::(oppai_cache::BeatmapCache::new( + http_client, + sql_client.clone(), + )); data.insert::(beatmap_cache::BeatmapMetaCache::new( - osu_client, + osu_client, sql_client, )); // Announcer @@ -196,7 +200,7 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult .await?; return Ok(()); } - add_user(msg.author.id, u.id, &*data)?; + add_user(msg.author.id, u.id, &*data).await?; msg.reply( &ctx, MessageBuilder::new() @@ -227,7 +231,7 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR let user: Option = osu.user(UserID::Auto(user), |f| f).await?; match user { Some(u) => { - add_user(target, u.id, &*data)?; + add_user(target, u.id, &*data).await?; msg.reply( &ctx, MessageBuilder::new() @@ -244,22 +248,15 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR Ok(()) } -fn add_user(target: serenity::model::id::UserId, user_id: u64, data: &TypeMap) -> Result<()> { - OsuSavedUsers::open(data).borrow_mut()?.insert( - target, - OsuUser { - id: user_id, - failures: None, - last_update: chrono::Utc::now(), - pp: vec![], - }, - ); - OsuUserBests::open(data) - .borrow_mut()? - .iter_mut() - .for_each(|(_, r)| { - r.remove(&target); - }); +async fn add_user(target: serenity::model::id::UserId, user_id: u64, data: &TypeMap) -> Result<()> { + let u = OsuUser { + user_id: target, + id: user_id, + failures: 0, + last_update: chrono::Utc::now(), + pp: [None, None, None, None], + }; + data.get::().unwrap().new_user(u).await?; Ok(()) } @@ -278,7 +275,7 @@ impl FromStr for ModeArg { } } -fn to_user_id_query( +async fn to_user_id_query( s: Option, data: &TypeMap, msg: &Message, @@ -289,12 +286,12 @@ fn to_user_id_query( None => msg.author.id, }; - let db = OsuSavedUsers::open(data); - let db = db.borrow()?; - db.get(&id) - .cloned() + data.get::() + .unwrap() + .by_user_id(id) + .await? .map(|u| UserID::ID(u.id)) - .ok_or(Error::msg("No saved account found")) + .ok_or_else(|| Error::msg("No saved account found")) } enum Nth { @@ -307,7 +304,7 @@ impl FromStr for Nth { fn from_str(s: &str) -> Result { if s == "--all" || s == "-a" || s == "##" { Ok(Nth::All) - } else if !s.starts_with("#") { + } else if !s.starts_with('#') { Err(Error::msg("Not an order")) } else { let v = s.split_at("#".len()).1.parse()?; @@ -385,13 +382,13 @@ async fn list_plays<'a>( .ok() .map(|pp| format!("{:.2}pp [?]", pp)) }) - .unwrap_or("-".to_owned())); + .unwrap_or_else(|| "-".to_owned())); r } } }) .collect::>() - .map(|v| v.unwrap_or("-".to_owned())) + .map(|v| v.unwrap_or_else(|_| "-".to_owned())) .collect::>(); let (beatmaps, pp) = future::join(beatmaps, pp).await; @@ -494,7 +491,7 @@ async fn list_plays<'a>( page + 1, total_pages )); - if let None = mode.to_oppai_mode() { + if mode.to_oppai_mode().is_none() { m.push_line("Note: star difficulty doesn't reflect mods applied."); } else { m.push_line("[?] means pp was predicted by oppai-rs."); @@ -521,7 +518,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu 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(), &*data, msg)?; + let user = to_user_id_query(args.single::().ok(), &*data, msg).await?; let osu = data.get::().unwrap(); let meta_cache = data.get::().unwrap(); @@ -529,7 +526,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu let user = osu .user(user, |f| f.mode(mode)) .await? - .ok_or(Error::msg("User not found"))?; + .ok_or_else(|| Error::msg("User not found"))?; match nth { Nth::Nth(nth) => { let recent_play = osu @@ -537,14 +534,14 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu .await? .into_iter() .last() - .ok_or(Error::msg("No such play"))?; + .ok_or_else(|| Error::msg("No such play"))?; 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",)) + m.content("Here is the play that you requested".to_string()) .embed(|m| { score_embed(&recent_play, &beatmap_mode, &content, &user).build(m) }) @@ -553,7 +550,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu .await?; // Save the beatmap... - cache::save_beatmap(&*data, msg.channel_id, &beatmap_mode)?; + cache::save_beatmap(&*data, msg.channel_id, &beatmap_mode).await?; } Nth::All => { let plays = osu @@ -585,7 +582,7 @@ impl FromStr for OptBeatmapset { #[max_args(2)] 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)?; + let b = cache::get_beatmap(&*data, msg.channel_id).await?; let beatmapset = args.find::().is_ok(); match b { @@ -636,7 +633,7 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult #[max_args(1)] 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)?; + let bm = cache::get_beatmap(&*data, msg.channel_id).await?; match bm { None => { @@ -648,11 +645,11 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul let m = bm.1; let username_arg = args.single::().ok(); let user_id = match username_arg.as_ref() { - Some(UsernameArg::Tagged(v)) => Some(v.clone()), + Some(UsernameArg::Tagged(v)) => Some(*v), None => Some(msg.author.id), _ => None, }; - let user = to_user_id_query(username_arg, &*data, msg)?; + let user = to_user_id_query(username_arg, &*data, msg).await?; let osu = data.get::().unwrap(); let oppai = data.get::().unwrap(); @@ -662,7 +659,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul let user = osu .user(user, |f| f) .await? - .ok_or(Error::msg("User not found"))?; + .ok_or_else(|| Error::msg("User not found"))?; let scores = osu .scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m)) .await?; @@ -681,11 +678,11 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul if let Some(user_id) = user_id { // Save to database - OsuUserBests::open(&*data) - .borrow_mut()? - .entry((bm.0.beatmap_id, bm.1)) - .or_default() - .insert(user_id, scores); + data.get::() + .unwrap() + .save(user_id, m, scores) + .await + .pls_ok(); } } } @@ -706,7 +703,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult .map(|ModeArg(t)| t) .unwrap_or(Mode::Std); - let user = to_user_id_query(args.single::().ok(), &*data, msg)?; + let user = to_user_id_query(args.single::().ok(), &*data, msg).await?; let meta_cache = data.get::().unwrap(); let osu = data.get::().unwrap(); @@ -714,7 +711,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult let user = osu .user(user, |f| f.mode(mode)) .await? - .ok_or(Error::msg("User not found"))?; + .ok_or_else(|| Error::msg("User not found"))?; match nth { Nth::Nth(nth) => { @@ -727,7 +724,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult let top_play = top_play .into_iter() .last() - .ok_or(Error::msg("No such play"))?; + .ok_or_else(|| Error::msg("No such play"))?; 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); @@ -747,7 +744,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult .await?; // Save the beatmap... - cache::save_beatmap(&*data, msg.channel_id, &beatmap)?; + cache::save_beatmap(&*data, msg.channel_id, &beatmap).await?; } Nth::All => { let plays = osu @@ -761,7 +758,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult 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 user = to_user_id_query(args.single::().ok(), &*data, msg).await?; let osu = data.get::().unwrap(); let cache = data.get::().unwrap(); let user = osu.user(user, |f| f.mode(mode)).await?; diff --git a/youmubot-osu/src/discord/oppai_cache.rs b/youmubot-osu/src/discord/oppai_cache.rs index 2ae82fb..ad86518 100644 --- a/youmubot-osu/src/discord/oppai_cache.rs +++ b/youmubot-osu/src/discord/oppai_cache.rs @@ -1,4 +1,5 @@ use std::{ffi::CString, sync::Arc}; +use youmubot_db_sql::{models::osu as models, Pool}; use youmubot_prelude::*; pub use oppai_rs::Accuracy as OppaiAccuracy; @@ -7,7 +8,7 @@ pub use oppai_rs::Accuracy as OppaiAccuracy; #[derive(Debug)] pub struct BeatmapContent { id: u64, - content: CString, + content: Arc, } /// the output of "one" oppai run. @@ -50,7 +51,7 @@ impl BeatmapContent { oppai.mods(mods.into()); let objects = oppai.num_objects(); let stars = oppai.stars(); - Ok(BeatmapInfo { stars, objects }) + Ok(BeatmapInfo { objects, stars }) } pub fn get_possible_pp_with( @@ -71,24 +72,21 @@ impl BeatmapContent { ]; let objects = oppai.num_objects(); let stars = oppai.stars(); - Ok((BeatmapInfo { stars, objects }, pp)) + Ok((BeatmapInfo { objects, stars }, pp)) } } /// A central cache for the beatmaps. pub struct BeatmapCache { client: ratelimit::Ratelimit, - cache: dashmap::DashMap>, + pool: Pool, } impl BeatmapCache { /// Create a new cache. - pub fn new(client: reqwest::Client) -> Self { + pub fn new(client: reqwest::Client, pool: Pool) -> Self { let client = ratelimit::Ratelimit::new(client, 5, std::time::Duration::from_secs(1)); - BeatmapCache { - client, - cache: dashmap::DashMap::new(), - } + BeatmapCache { client, pool } } async fn download_beatmap(&self, id: u64) -> Result { @@ -103,20 +101,39 @@ impl BeatmapCache { .await?; Ok(BeatmapContent { id, - content: CString::new(content.into_iter().collect::>())?, + content: Arc::new(CString::new(content.into_iter().collect::>())?), }) } + async fn get_beatmap_db(&self, id: u64) -> Result> { + Ok(models::CachedBeatmapContent::by_id(id as i64, &self.pool) + .await? + .map(|v| BeatmapContent { + id, + content: Arc::new(CString::new(v.content).unwrap()), + })) + } + + async fn save_beatmap(&self, b: &BeatmapContent) -> Result<()> { + let mut bc = models::CachedBeatmapContent { + beatmap_id: b.id as i64, + cached_at: chrono::Utc::now(), + content: b.content.as_ref().clone().into_bytes(), + }; + bc.store(&self.pool).await?; + Ok(()) + } + /// Get a beatmap from the cache. - 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?)); + pub async fn get_beatmap(&self, id: u64) -> Result { + match self.get_beatmap_db(id).await? { + Some(v) => Ok(v), + None => { + let m = self.download_beatmap(id).await?; + self.save_beatmap(&m).await?; + Ok(m) + } } - 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 76d4007..8c7c627 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -28,12 +28,15 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR let mode = args.single::().map(|v| v.0).unwrap_or(Mode::Std); let guild = m.guild_id.expect("Guild-only command"); let member_cache = data.get::().unwrap(); - let users = OsuSavedUsers::open(&*data).borrow()?.clone(); - let users = users + let users = data + .get::() + .unwrap() + .all() + .await? .into_iter() - .map(|(user_id, osu_user)| async move { + .map(|osu_user| async move { member_cache - .query(&ctx, user_id, guild) + .query(&ctx, osu_user.user_id, guild) .await .and_then(|member| { osu_user @@ -41,11 +44,11 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR .get(mode as usize) .cloned() .and_then(|pp| pp) - .map(|pp| (pp, member.distinct(), osu_user.last_update.clone())) + .map(|pp| (pp, member.distinct(), osu_user.last_update)) }) }) .collect::>() - .filter_map(|v| future::ready(v)) + .filter_map(future::ready) .collect::>() .await; let last_update = users.iter().map(|(_, _, a)| a).min().cloned(); @@ -176,7 +179,7 @@ pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> Comma } Some(v) => v, }; - let bm = match get_beatmap(&*data, m.channel_id)? { + let bm = match get_beatmap(&*data, m.channel_id).await? { Some(bm) => bm, None => { m.reply(&ctx, "No beatmap queried on this channel.").await?; @@ -191,15 +194,15 @@ pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> Comma // Run a check on everyone in the server basically. let all_server_users: Vec<(UserId, Vec)> = { let osu = data.get::().unwrap(); - let osu_users = OsuSavedUsers::open(&*data); - let osu_users = osu_users - .borrow()? - .iter() - .map(|(&user_id, osu_user)| (user_id, osu_user.id)) - .collect::>(); + let osu_users = data + .get::() + .unwrap() + .all() + .await? + .into_iter() + .map(|osu_user| (osu_user.user_id, osu_user.id)); let beatmap_id = bm.0.beatmap_id; osu_users - .into_iter() .map(|(user_id, osu_id)| { member_cache .query(&ctx, user_id, guild) @@ -222,12 +225,13 @@ pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> Comma let updated_users = all_server_users.len(); // Update everything. { - let mut osu_user_bests = OsuUserBests::open(&*data); - let mut osu_user_bests = osu_user_bests.borrow_mut()?; - let user_bests = osu_user_bests.entry((bm.0.beatmap_id, bm.1)).or_default(); - all_server_users.into_iter().for_each(|(member, scores)| { - user_bests.insert(member, scores); - }) + let db = data.get::().unwrap(); + all_server_users + .into_iter() + .map(|(u, scores)| db.save(u, mode, scores)) + .collect::>() + .try_collect::<()>() + .await?; } // Signal update complete. running_reaction.delete(&ctx).await.ok(); @@ -255,7 +259,7 @@ pub async fn leaderboard(ctx: &Context, m: &Message, args: Args) -> CommandResul let sort_order = OrderBy::from(args.rest()); let data = ctx.data.read().await; - let bm = match get_beatmap(&*data, m.channel_id)? { + let bm = match get_beatmap(&*data, m.channel_id).await? { Some(bm) => bm, None => { m.reply(&ctx, "No beatmap queried on this channel.").await?; @@ -272,7 +276,6 @@ async fn show_leaderboard( order: OrderBy, ) -> CommandResult { let data = ctx.data.read().await; - let mut osu_user_bests = OsuUserBests::open(&*data); // Get oppai map. let mode = bm.1; @@ -294,8 +297,12 @@ async fn show_leaderboard( // 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); + let user = data + .get::() + .unwrap() + .by_user_id(m.author.id) + .await? + .map(|v| v.id); if let Some(id) = user { let osu = data.get::().unwrap(); if let Ok(scores) = osu @@ -303,11 +310,11 @@ async fn show_leaderboard( .await { if !scores.is_empty() { - osu_user_bests - .borrow_mut()? - .entry((bm.0.beatmap_id, bm.1)) - .or_default() - .insert(m.author.id, scores); + data.get::() + .unwrap() + .save(m.author.id, mode, scores) + .await + .pls_ok(); } } } @@ -316,39 +323,27 @@ async fn show_leaderboard( let guild = m.guild_id.expect("Guild-only command"); let member_cache = data.get::().unwrap(); let scores = { - const NO_SCORES: &'static str = "No scores have been recorded for this beatmap."; + const NO_SCORES: &str = "No scores have been recorded for this beatmap."; - let users = osu_user_bests - .borrow()? - .get(&(bm.0.beatmap_id, bm.1)) - .cloned(); - let users = match users { - None => { - m.reply(&ctx, NO_SCORES).await?; - return Ok(()); - } - Some(v) if v.is_empty() => { - m.reply(&ctx, NO_SCORES).await?; - return Ok(()); - } - Some(v) => v, - }; + let scores = data + .get::() + .unwrap() + .by_beatmap(bm.0.beatmap_id, bm.1) + .await?; + if scores.is_empty() { + m.reply(&ctx, NO_SCORES).await?; + return Ok(()); + } - let mut scores: Vec<(f64, String, Score)> = users + let mut scores: Vec<(f64, String, Score)> = scores .into_iter() - .map(|(user_id, scores)| { + .map(|(user_id, score)| { member_cache .query(&ctx, user_id, guild) - .map(|m| m.map(move |m| (m.distinct(), scores))) + .map(|m| m.map(move |m| (m.distinct(), score))) }) .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(future::ready) .filter_map(|(user, score)| { future::ready( score @@ -395,8 +390,8 @@ async fn show_leaderboard( return Box::pin(future::ready(Ok(false))); } let total_len = scores.len(); - let scores = (&scores[start..end]).iter().cloned().collect::>(); - let bm = (bm.0.clone(), bm.1.clone()); + let scores = (&scores[start..end]).to_vec(); + let bm = (bm.0.clone(), bm.1); Box::pin(async move { // username width let uw = scores @@ -428,7 +423,7 @@ async fn show_leaderboard( .iter() .map(|(pp, _, s)| match order { OrderBy::PP => format!("{:.2}", pp), - OrderBy::Score => format!("{}", crate::discord::embeds::grouped_number(s.score)), + OrderBy::Score => crate::discord::embeds::grouped_number(s.score), }) .collect::>(); let pw = pp.iter().map(|v| v.len()).max().unwrap_or(pp_label.len()); @@ -512,11 +507,10 @@ async fn show_leaderboard( (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE, )); if let crate::models::ApprovalStatus::Ranked(_) = bm.0.approval { - } else { - if order == OrderBy::PP { - content.push_line("PP was calculated by `oppai-rs`, **not** official values."); - } + } else if order == OrderBy::PP { + content.push_line("PP was calculated by `oppai-rs`, **not** official values."); } + m.edit(&ctx, |f| f.content(content.build())).await?; Ok(true) }) diff --git a/youmubot-osu/src/lib.rs b/youmubot-osu/src/lib.rs index 129fc0a..7122a57 100644 --- a/youmubot-osu/src/lib.rs +++ b/youmubot-osu/src/lib.rs @@ -39,7 +39,7 @@ impl Client { REQUESTS_PER_MINUTE, std::time::Duration::from_secs(60), ); - Client { key, client } + Client { client, key } } pub(crate) async fn build_request(&self, url: &str) -> Result { diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index 9333035..98ebefd 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -131,10 +131,7 @@ impl Difficulty { /// Format the difficulty info into a short summary. pub fn format_info(&self, mode: Mode, mods: Mods, original_beatmap: &Beatmap) -> String { - let is_not_ranked = match original_beatmap.approval { - ApprovalStatus::Ranked(_) => false, - _ => true, - }; + let is_not_ranked = !matches!(original_beatmap.approval, ApprovalStatus::Ranked(_)); let three_lines = is_not_ranked; let bpm = (self.bpm * 100.0).round() / 100.0; MessageBuilder::new() @@ -243,6 +240,18 @@ pub enum Mode { Mania, } +impl From for Mode { + fn from(n: u8) -> Self { + match n { + 0 => Self::Std, + 1 => Self::Taiko, + 2 => Self::Catch, + 3 => Self::Mania, + _ => panic!("Unknown mode {}", n), + } + } +} + impl fmt::Display for Mode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use Mode::*; @@ -292,7 +301,7 @@ impl Mode { } /// Returns the mode string in the new convention. - pub fn to_str_new_site(&self) -> &'static str { + pub fn as_str_new_site(&self) -> &'static str { match self { Mode::Std => "osu", Mode::Taiko => "taiko", @@ -332,7 +341,7 @@ pub struct Beatmap { pub pass_count: u64, } -const NEW_MODE_NAMES: [&'static str; 4] = ["osu", "taiko", "fruits", "mania"]; +const NEW_MODE_NAMES: [&str; 4] = ["osu", "taiko", "fruits", "mania"]; impl Beatmap { pub fn beatmapset_link(&self) -> String { @@ -368,10 +377,11 @@ impl Beatmap { "/b/{}{}{}", self.beatmap_id, match override_mode { - Some(mode) if mode != self.mode => format!("/{}", mode.to_str_new_site()), + Some(mode) if mode != self.mode => format!("/{}", mode.as_str_new_site()), _ => "".to_owned(), }, - mods.map(|m| format!("{}", m)).unwrap_or("".to_owned()), + mods.map(|m| format!("{}", m)) + .unwrap_or_else(|| "".to_owned()), ) } @@ -410,7 +420,7 @@ impl UserEvent { let mode: Mode = Mode::parse_from_display(captures.get(2)?.as_str())?; Some(UserEventRank { beatmap_id: self.beatmap_id?, - date: self.date.clone(), + date: self.date, mode, rank, }) diff --git a/youmubot-osu/src/models/mods.rs b/youmubot-osu/src/models/mods.rs index e830728..d6c2660 100644 --- a/youmubot-osu/src/models/mods.rs +++ b/youmubot-osu/src/models/mods.rs @@ -43,7 +43,7 @@ bitflags::bitflags! { } } -const MODS_WITH_NAMES: &[(Mods, &'static str)] = &[ +const MODS_WITH_NAMES: &[(Mods, &str)] = &[ (Mods::NF, "NF"), (Mods::EZ, "EZ"), (Mods::TD, "TD"), @@ -75,7 +75,7 @@ impl std::str::FromStr for Mods { fn from_str(mut s: &str) -> Result { let mut res = Self::default(); // Strip leading + - if s.starts_with("+") { + if s.starts_with('+') { s = &s[1..]; } while s.len() >= 2 { @@ -109,7 +109,7 @@ impl std::str::FromStr for Mods { v => return Err(format!("{} is not a valid mod", v)), } } - if s.len() > 0 { + if !s.is_empty() { Err("String of odd length is not a mod string".to_owned()) } else { Ok(res) diff --git a/youmubot-osu/src/models/parse.rs b/youmubot-osu/src/models/parse.rs index 23ba8c9..40eb4f4 100644 --- a/youmubot-osu/src/models/parse.rs +++ b/youmubot-osu/src/models/parse.rs @@ -82,7 +82,7 @@ impl TryFrom for User { played_time: raw .total_seconds_played .map(parse_duration) - .unwrap_or(Ok(Duration::from_secs(0)))?, + .unwrap_or_else(|| Ok(Duration::from_secs(0)))?, ranked_score: raw.ranked_score.map(parse_from_str).unwrap_or(Ok(0))?, total_score: raw.total_score.map(parse_from_str).unwrap_or(Ok(0))?, count_ss: raw.count_rank_ss.map(parse_from_str).unwrap_or(Ok(0))?, diff --git a/youmubot-prelude/Cargo.toml b/youmubot-prelude/Cargo.toml index 0ed83c2..1307949 100644 --- a/youmubot-prelude/Cargo.toml +++ b/youmubot-prelude/Cargo.toml @@ -12,6 +12,7 @@ async-trait = "0.1" futures-util = "0.3" tokio = { version = "1", features = ["time"] } youmubot-db = { path = "../youmubot-db" } +youmubot-db-sql = { path = "../youmubot-db-sql" } reqwest = "0.11" chrono = "0.4" flume = "0.10" diff --git a/youmubot-prelude/src/announcer.rs b/youmubot-prelude/src/announcer.rs index 495be4f..8cff956 100644 --- a/youmubot-prelude/src/announcer.rs +++ b/youmubot-prelude/src/announcer.rs @@ -62,11 +62,11 @@ impl MemberToChannels { .into_iter() .map(|(guild, channel)| { member_cache - .query(http.clone(), u.into(), guild) + .query(http.clone(), u, guild) .map(move |t| t.map(|_| channel)) }) .collect::>() - .filter_map(|v| ready(v)) + .filter_map(ready) .collect() .await } @@ -105,9 +105,10 @@ impl AnnouncerHandler { key: &'static str, announcer: impl Announcer + Send + Sync + 'static, ) -> &mut Self { - if let Some(_) = self + if self .announcers .insert(key, RwLock::new(Box::new(announcer))) + .is_some() { panic!( "Announcer keys must be unique: another announcer with key `{}` was found", @@ -127,7 +128,7 @@ impl AnnouncerHandler { .borrow()? .get(key) .map(|m| m.iter().map(|(a, b)| (*a, *b)).collect()) - .unwrap_or_else(|| vec![]); + .unwrap_or_else(Vec::new); Ok(data) } @@ -149,7 +150,7 @@ impl AnnouncerHandler { /// Start the AnnouncerHandler, looping forever. /// /// It will run all the announcers in sequence every *cooldown* seconds. - pub async fn scan(self, cooldown: std::time::Duration) -> () { + 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().await.insert::(keys.clone()); diff --git a/youmubot-prelude/src/args.rs b/youmubot-prelude/src/args.rs index 80175d7..69181ac 100644 --- a/youmubot-prelude/src/args.rs +++ b/youmubot-prelude/src/args.rs @@ -11,7 +11,7 @@ mod duration { /// Parse a single duration unit fn parse_duration_string(s: &str) -> Result { // We reject the empty case - if s == "" { + if s.is_empty() { return Err(Error::msg("empty strings are not valid durations")); } struct ParseStep { @@ -59,7 +59,7 @@ mod duration { impl std::str::FromStr for Duration { type Err = Error; fn from_str(s: &str) -> Result { - parse_duration_string(s).map(|v| Duration(v)) + parse_duration_string(s).map(Duration) } } diff --git a/youmubot-prelude/src/lib.rs b/youmubot-prelude/src/lib.rs index 4d4c512..39469cb 100644 --- a/youmubot-prelude/src/lib.rs +++ b/youmubot-prelude/src/lib.rs @@ -40,6 +40,13 @@ impl TypeMapKey for HTTPClient { type Value = reqwest::Client; } +/// The SQL client. +pub struct SQLClient; + +impl TypeMapKey for SQLClient { + type Value = youmubot_db_sql::Pool; +} + pub mod prelude_commands { use crate::announcer::ANNOUNCERCOMMANDS_GROUP; use serenity::{ diff --git a/youmubot-prelude/src/member_cache.rs b/youmubot-prelude/src/member_cache.rs index a89d3bb..e46a439 100644 --- a/youmubot-prelude/src/member_cache.rs +++ b/youmubot-prelude/src/member_cache.rs @@ -27,7 +27,7 @@ impl MemberCache { let now = Utc::now(); // Check cache if let Some(r) = self.0.get(&(user_id, guild_id)) { - if &r.1 > &now { + if r.1 > now { return r.0.clone(); } } diff --git a/youmubot-prelude/src/pagination.rs b/youmubot-prelude/src/pagination.rs index 3f6c0ea..9c6b0bd 100644 --- a/youmubot-prelude/src/pagination.rs +++ b/youmubot-prelude/src/pagination.rs @@ -10,8 +10,8 @@ use serenity::{ use std::convert::TryFrom; use tokio::time as tokio_time; -const ARROW_RIGHT: &'static str = "โžก๏ธ"; -const ARROW_LEFT: &'static str = "โฌ…๏ธ"; +const ARROW_RIGHT: &str = "โžก๏ธ"; +const ARROW_LEFT: &str = "โฌ…๏ธ"; /// A trait that provides the implementation of a paginator. #[async_trait::async_trait] diff --git a/youmubot-prelude/src/ratelimit.rs b/youmubot-prelude/src/ratelimit.rs index f7557dc..47b6cc3 100644 --- a/youmubot-prelude/src/ratelimit.rs +++ b/youmubot-prelude/src/ratelimit.rs @@ -30,15 +30,15 @@ impl Ratelimit { }); Self { inner, - send, recv, + send, 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> { + pub async fn borrow(&self) -> Result + '_> { self.recv.recv_async().await?; Ok(RatelimitGuard { inner: &self.inner, @@ -58,7 +58,7 @@ impl<'a, T> Deref for RatelimitGuard<'a, T> { impl<'a, T> Drop for RatelimitGuard<'a, T> { fn drop(&mut self) { let send = self.send.clone(); - let wait_time = self.wait_time.clone(); + let wait_time = *self.wait_time; tokio::spawn(async move { tokio::time::sleep(wait_time).await; send.send_async(()).await.ok(); diff --git a/youmubot-prelude/src/setup.rs b/youmubot-prelude/src/setup.rs index 081289f..f3e6475 100644 --- a/youmubot-prelude/src/setup.rs +++ b/youmubot-prelude/src/setup.rs @@ -4,14 +4,29 @@ 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 TypeMap) { +pub async fn setup_prelude( + db_path: impl AsRef, + sql_path: impl AsRef, + data: &mut TypeMap, +) { // Setup the announcer DB. - crate::announcer::AnnouncerChannels::insert_into(data, db_path.join("announcers.yaml")) - .expect("Announcers DB set up"); + crate::announcer::AnnouncerChannels::insert_into( + data, + db_path.as_ref().join("announcers.yaml"), + ) + .expect("Announcers DB set up"); + + // Set up the database + let sql_pool = youmubot_db_sql::connect(sql_path) + .await + .expect("SQL database set up"); // Set up the HTTP client. data.insert::(reqwest::Client::new()); // Set up the member cache. data.insert::(std::sync::Arc::new(crate::MemberCache::default())); + + // Set up the SQL client. + data.insert::(sql_pool); } diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index f274df7..92ea3d9 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -1,4 +1,3 @@ -use dotenv; use dotenv::var; use serenity::{ client::bridge::gateway::GatewayIntents, @@ -118,12 +117,19 @@ async fn main() { { let mut data = client.data.write().await; let db_path = var("DBPATH") - .map(|v| std::path::PathBuf::from(v)) + .map(std::path::PathBuf::from) .unwrap_or_else(|e| { println!("No DBPATH set up ({:?}), using `/data`", e); - std::path::PathBuf::from("data") + std::path::PathBuf::from("/data") }); - youmubot_prelude::setup::setup_prelude(&db_path, &mut data); + let sql_path = var("SQLPATH") + .map(std::path::PathBuf::from) + .unwrap_or_else(|e| { + let res = db_path.join("youmubot.db"); + println!("No SQLPATH set up ({:?}), using `{:?}`", e, res); + res + }); + youmubot_prelude::setup::setup_prelude(&db_path, sql_path, &mut data).await; // Setup core #[cfg(feature = "core")] youmubot_core::setup(&db_path, &client, &mut data).expect("Setup db should succeed"); @@ -166,7 +172,7 @@ async fn setup_framework(token: &str) -> StandardFramework { let fw = StandardFramework::new() .configure(|c| { c.with_whitespace(false) - .prefix(&var("PREFIX").unwrap_or("y!".to_owned())) + .prefix(&var("PREFIX").unwrap_or_else(|_| "y!".to_owned())) .delimiters(vec![" / ", "/ ", " /", "/"]) .owners([owner.id].iter().cloned().collect()) }) @@ -246,7 +252,7 @@ async fn on_dispatch_error(ctx: &Context, msg: &Message, error: DispatchError) { "๐Ÿ˜• I can only handle at most **{}** arguments, but I got **{}**!", max, given ), - DispatchError::OnlyForGuilds => format!("๐Ÿ”‡ This command cannot be used in DMs."), + DispatchError::OnlyForGuilds => "๐Ÿ”‡ This command cannot be used in DMs.".to_owned(), _ => return, }, )