Move to SQLite (#13)

This commit is contained in:
Natsu Kagami 2021-06-19 22:36:17 +09:00 committed by GitHub
parent 750ddb7762
commit 1799b70bc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 2122 additions and 394 deletions

View file

@ -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"

View file

@ -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:

2
.gitignore vendored
View file

@ -3,3 +3,5 @@ target
*.yaml
cargo-remote
.vscode
youmubot.db
youmubot.db-*

454
Cargo.lock generated
View file

@ -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",
]

View file

@ -3,6 +3,7 @@
members = [
"youmubot-prelude",
"youmubot-db",
"youmubot-db-sql",
"youmubot-core",
"youmubot-cf",
"youmubot-osu",

View file

@ -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?;

View file

@ -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)

View file

@ -77,11 +77,11 @@ impl ContestCache {
}
async fn get_from_list(&self, contest_id: u64) -> Result<Contest> {
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(),

View file

@ -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::<UsernameArg>()
.unwrap_or(UsernameArg::mention(m.author.id));
.unwrap_or_else(|_| UsernameArg::mention(m.author.id));
let http = data.get::<CFClient>().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::<Vec<_>>()
db.iter().map(|(k, v)| (*k, v.clone())).collect::<Vec<_>>()
};
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::<stream::FuturesUnordered<_>>()
.filter_map(|v| future::ready(v))
.filter_map(future::ready)
.collect::<HashMap<_, _>>()
.await;
let http = data.get::<CFClient>().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)));

View file

@ -51,7 +51,7 @@ pub async fn watch_contest(
}
})
.collect::<stream::FuturesUnordered<_>>()
.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<Change> {
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)
}
}

View file

@ -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?;
}

View file

@ -28,7 +28,9 @@ pub async fn soft_ban(ctx: &Context, msg: &Message, mut args: Args) -> CommandRe
} else {
Some(args.single::<args::Duration>()?)
};
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(())
}

View file

@ -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::<stream::FuturesUnordered<_>>()
.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())

View file

@ -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 {

View file

@ -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<Vec<String>, Error> {
const MAX_CHOICES: usize = 15;
// All the defined reactions.
const REACTIONS: [&'static str; 90] = [
const REACTIONS: [&str; 90] = [
"😀", "😁", "😂", "🤣", "😃", "😄", "😅", "😆", "😉", "😊", "😋", "😎", "😍", "😘", "🥰", "😗",
"😙", "😚", "☺️", "🙂", "🤗", "🤩", "🤔", "🤨", "😐", "😑", "😶", "🙄", "😏", "😣", "😥", "😮",
"🤐", "😯", "😪", "😫", "😴", "😌", "😛", "😜", "😝", "🤤", "😒", "😓", "😔", "😕", "🙃", "🤑",

View file

@ -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.

View file

@ -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",

View file

@ -0,0 +1,14 @@
[package]
name = "youmubot-db-sql"
version = "0.1.0"
authors = ["Natsu Kagami <nki@nkagami.me>"]
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"

View file

@ -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
);

View file

@ -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
);

View file

@ -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<u8>\"\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<u8>",
"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<u8>\"\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<u8>",
"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<u8>\"\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<u8>",
"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<u8>\"\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<u8>",
"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<u8>\"\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<u8>",
"ordinal": 3,
"type_info": "Blob"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false
]
}
}
}

View file

@ -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<Path>) -> Result<Pool> {
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<T, E = Error> = std::result::Result<T, E>;
/// 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");
}

View file

@ -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<chrono::Utc>;
pub mod osu;
pub mod osu_user;
/// Map a `fetch_many` result to a normal result.
pub(crate) async fn map_many_result<T, E, W>(
item: Result<either::Either<W, T>, E>,
) -> Option<Result<T>>
where
E: Into<Error>,
{
match item {
Ok(v) => v.right().map(Ok),
Err(e) => Some(Err(e.into())),
}
}

View file

@ -0,0 +1,319 @@
use crate::models::*;
pub struct LastBeatmap {
pub channel_id: i64,
pub beatmap: Vec<u8>,
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<Option<LastBeatmap>> {
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<u8>,
}
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<Vec<Self>> {
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<u8>"
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<Vec<Self>> {
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<u8>"
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<u8>,
}
impl CachedBeatmap {
/// Get a cached beatmap by its id.
pub async fn by_id(
id: i64,
mode: u8,
conn: impl Executor<'_, Database = Database>,
) -> Result<Option<Self>> {
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<u8>"
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<Vec<Self>> {
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<u8>"
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<u8>,
}
impl CachedBeatmapContent {
/// Get a cached beatmap by its id.
pub async fn by_id(
id: i64,
conn: impl Executor<'_, Database = Database>,
) -> Result<Option<Self>> {
query_as!(
Self,
r#"SELECT
beatmap_id as "beatmap_id: i64",
cached_at as "cached_at: DateTime",
content as "content: Vec<u8>"
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(())
}
}

View file

@ -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<f32>,
pub pp_taiko: Option<f32>,
pub pp_mania: Option<f32>,
pub pp_catch: Option<f32>,
/// 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<Option<Self>>
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<Option<Self>>
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<Item = Result<Self>> + '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(())
}
}

View file

@ -25,7 +25,7 @@ where
{
/// Load the DB from a path.
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Database<T>, DBError> {
Ok(Database::<T>::load_from_path(path)?)
Database::<T>::load_from_path(path)
}
/// Insert into a ShareMap.

View file

@ -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]

View file

@ -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::<OsuSavedUsers>().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::<stream::FuturesOrdered<_>>()
.try_collect::<Vec<_>>()
.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::<stream::FuturesOrdered<_>>()
.try_collect::<Vec<_>>()
.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::<stream::FuturesUnordered<_>>()
.collect::<HashMap<_, _>>()
.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<ChannelId>,
mode: Mode,
) -> Result<Option<f64>, Error> {
) -> Result<Option<f32>, 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<Vec<(u8, Score)>, Error> {
@ -265,20 +239,14 @@ impl<'a> CollectedScore<'a> {
async fn send_message(self, ctx: &Context) -> Result<Vec<Message>> {
let (bm, content) = self.get_beatmap(&ctx).await?;
self.channels
.into_iter()
.iter()
.map(|c| self.send_message_to(*c, ctx, &bm, &content))
.collect::<stream::FuturesUnordered<_>>()
.try_collect()
.await
}
async fn get_beatmap(
&self,
ctx: &Context,
) -> Result<(
BeatmapWithMode,
impl std::ops::Deref<Target = BeatmapContent>,
)> {
async fn get_beatmap(&self, ctx: &Context) -> Result<(BeatmapWithMode, BeatmapContent)> {
let data = ctx.data.read().await;
let cache = data.get::<BeatmapMetaCache>().unwrap();
let oppai = data.get::<BeatmapCache>().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)
}
}

View file

@ -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<Client>,
cache: DashMap<(u64, Mode), Beatmap>,
beatmapsets: DashMap<u64, Vec<u64>>,
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<Client>) -> Self {
BeatmapMetaCache {
client,
cache: DashMap::new(),
beatmapsets: DashMap::new(),
pub fn new(client: Arc<Client>, pool: Pool) -> Self {
BeatmapMetaCache { client, pool }
}
#[allow(clippy::wrong_self_convention)]
fn to_cached_beatmap(beatmap: &Beatmap, mode: Option<Mode>) -> 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<Mode>) -> Result<Beatmap> {
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<Option<Beatmap>> {
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<Beatmap> {
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<Beatmap> {
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<Vec<Beatmap>> {
match self.beatmapsets.get(&id).map(|v| v.clone()) {
Some(v) => {
v.into_iter()
.map(|id| self.get_beatmap_default(id))
.collect::<stream::FuturesOrdered<_>>()
.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)
}
}

View file

@ -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::<OsuLastBeatmap>()
.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<Option<BeatmapWithMode>> {
let db = OsuLastBeatmap::open(data);
let db = db.borrow()?;
Ok(db
.get(&channel_id)
.cloned()
.map(|(a, b)| BeatmapWithMode(a, b)))
data.get::<OsuLastBeatmap>()
.unwrap()
.by_channel(channel_id)
.await
.map(|v| v.map(|(bm, mode)| BeatmapWithMode(bm, mode)))
}

View file

@ -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<HashMap<UserId, OsuUser>>;
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<Vec<OsuUser>> {
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<Option<OsuUser>> {
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<HashMap<ChannelId, (Beatmap, Mode)>>;
pub struct OsuLastBeatmap(Pool);
/// Save each beatmap's plays by user.
pub type OsuUserBests =
DB<HashMap<(u64, Mode) /* Beatmap ID and Mode */, HashMap<UserId, Vec<Score>>>>;
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<ChannelId>) -> Result<Option<(Beatmap, Mode)>> {
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<ChannelId>,
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<Vec<(UserId, Score)>> {
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<UserId>,
mode: Mode,
scores: impl IntoIterator<Item = Score>,
) -> 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::<stream::FuturesUnordered<_>>()
.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<Utc>,
#[serde(default)]
pub pp: Vec<Option<f64>>,
pub pp: [Option<f32>; 4],
/// More than 5 failures => gone
pub failures: Option<u8>,
pub failures: u8,
}
impl From<OsuUser> 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<model::OsuUser> 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<HashMap<UserId, OsuUser>>;
/// An osu! saved user.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OsuUser {
pub id: u64,
pub last_update: DateTime<Utc>,
#[serde(default)]
pub pp: Vec<Option<f64>>,
/// More than 5 failures => gone
pub failures: Option<u8>,
}
/// Save each channel's last requested beatmap.
pub type OsuLastBeatmap = DB<HashMap<ChannelId, (Beatmap, Mode)>>;
/// Save each beatmap's plays by user.
pub type OsuUserBests =
DB<HashMap<(u64, Mode) /* Beatmap ID and Mode */, HashMap<UserId, Vec<Score>>>>;
}

View file

@ -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)
}

View file

@ -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<BeatmapInfo>)>,
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 ")

View file

@ -50,6 +50,7 @@ pub fn hook<'a>(
msg.channel_id,
&bm,
)
.await
.pls_ok();
}
EmbedType::Beatmapset(b) => {

View file

@ -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::<SQLClient>().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>(OsuSavedUsers::new(sql_client.clone()));
data.insert::<OsuLastBeatmap>(OsuLastBeatmap::new(sql_client.clone()));
data.insert::<OsuUserBests>(OsuUserBests::new(sql_client.clone()));
// Locks
data.insert::<server_rank::update_lock::UpdateLock>(
@ -75,9 +76,12 @@ pub fn setup(
};
let osu_client = Arc::new(make_client());
data.insert::<OsuClient>(osu_client.clone());
data.insert::<oppai_cache::BeatmapCache>(oppai_cache::BeatmapCache::new(http_client));
data.insert::<oppai_cache::BeatmapCache>(oppai_cache::BeatmapCache::new(
http_client,
sql_client.clone(),
));
data.insert::<beatmap_cache::BeatmapMetaCache>(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<User> = 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::<OsuSavedUsers>().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<UsernameArg>,
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::<OsuSavedUsers>()
.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<Self, Self::Err> {
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::<stream::FuturesOrdered<_>>()
.map(|v| v.unwrap_or("-".to_owned()))
.map(|v| v.unwrap_or_else(|_| "-".to_owned()))
.collect::<Vec<String>>();
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::<Nth>().unwrap_or(Nth::All);
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg)?;
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg).await?;
let osu = data.get::<OsuClient>().unwrap();
let meta_cache = data.get::<BeatmapMetaCache>().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::<OptBeatmapset>().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::<UsernameArg>().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::<OsuClient>().unwrap();
let oppai = data.get::<BeatmapCache>().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::<OsuUserBests>()
.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::<UsernameArg>().ok(), &*data, msg)?;
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg).await?;
let meta_cache = data.get::<BeatmapMetaCache>().unwrap();
let osu = data.get::<OsuClient>().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::<UsernameArg>().ok(), &*data, msg)?;
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg).await?;
let osu = data.get::<OsuClient>().unwrap();
let cache = data.get::<BeatmapMetaCache>().unwrap();
let user = osu.user(user, |f| f.mode(mode)).await?;

View file

@ -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<CString>,
}
/// 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<reqwest::Client>,
cache: dashmap::DashMap<u64, Arc<BeatmapContent>>,
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<BeatmapContent> {
@ -103,20 +101,39 @@ impl BeatmapCache {
.await?;
Ok(BeatmapContent {
id,
content: CString::new(content.into_iter().collect::<Vec<_>>())?,
content: Arc::new(CString::new(content.into_iter().collect::<Vec<_>>())?),
})
}
async fn get_beatmap_db(&self, id: u64) -> Result<Option<BeatmapContent>> {
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<impl std::ops::Deref<Target = BeatmapContent>> {
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<BeatmapContent> {
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())
}
}

View file

@ -28,12 +28,15 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
let mode = args.single::<ModeArg>().map(|v| v.0).unwrap_or(Mode::Std);
let guild = m.guild_id.expect("Guild-only command");
let member_cache = data.get::<MemberCache>().unwrap();
let users = OsuSavedUsers::open(&*data).borrow()?.clone();
let users = users
let users = data
.get::<OsuSavedUsers>()
.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::<stream::FuturesUnordered<_>>()
.filter_map(|v| future::ready(v))
.filter_map(future::ready)
.collect::<Vec<_>>()
.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<Score>)> = {
let osu = data.get::<OsuClient>().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::<Vec<_>>();
let osu_users = data
.get::<OsuSavedUsers>()
.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::<OsuUserBests>().unwrap();
all_server_users
.into_iter()
.map(|(u, scores)| db.save(u, mode, scores))
.collect::<stream::FuturesUnordered<_>>()
.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::<OsuSavedUsers>()
.unwrap()
.by_user_id(m.author.id)
.await?
.map(|v| v.id);
if let Some(id) = user {
let osu = data.get::<OsuClient>().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::<OsuUserBests>()
.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::<MemberCache>().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::<OsuUserBests>()
.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::<stream::FuturesUnordered<_>>()
.filter_map(|v| future::ready(v))
.flat_map(|(user, scores)| {
scores
.into_iter()
.map(move |v| future::ready((user.clone(), v.clone())))
.collect::<stream::FuturesUnordered<_>>()
})
.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::<Vec<_>>();
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::<Vec<_>>();
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)
})

View file

@ -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<reqwest::RequestBuilder> {

View file

@ -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<u8> 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,
})

View file

@ -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<Self, Self::Err> {
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)

View file

@ -82,7 +82,7 @@ impl TryFrom<raw::User> 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))?,

View file

@ -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"

View file

@ -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::<FuturesUnordered<_>>()
.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::<Vec<_>>();
self.data.write().await.insert::<Self>(keys.clone());

View file

@ -11,7 +11,7 @@ mod duration {
/// Parse a single duration unit
fn parse_duration_string(s: &str) -> Result<StdDuration> {
// 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<Self, Self::Err> {
parse_duration_string(s).map(|v| Duration(v))
parse_duration_string(s).map(Duration)
}
}

View file

@ -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::{

View file

@ -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();
}
}

View file

@ -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]

View file

@ -30,15 +30,15 @@ impl<T> Ratelimit<T> {
});
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<impl Deref<Target = T> + 'a> {
pub async fn borrow(&self) -> Result<impl Deref<Target = T> + '_> {
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();

View file

@ -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<Path>,
sql_path: impl AsRef<Path>,
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::<crate::HTTPClient>(reqwest::Client::new());
// Set up the member cache.
data.insert::<crate::MemberCache>(std::sync::Arc::new(crate::MemberCache::default()));
// Set up the SQL client.
data.insert::<crate::SQLClient>(sql_pool);
}

View file

@ -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,
},
)