From 74f554e058921dda943d732aeade114fb45ab8e4 Mon Sep 17 00:00:00 2001 From: prescientmoon Date: Mon, 23 Sep 2024 19:46:53 +0200 Subject: [PATCH] Better error handling --- Cargo.lock | 464 ++++++++++-------- Cargo.toml | 19 +- flake.lock | 43 +- flake.nix | 47 +- src/arcaea/achievement.rs | 26 +- src/arcaea/play.rs | 58 ++- src/{cli/mod.rs => bin/cli/command.rs} | 8 +- src/{cli => bin/cli/commands}/analyse.rs | 6 +- src/bin/cli/commands/mod.rs | 2 + .../cli/commands}/prepare_jackets.rs | 10 +- src/{ => bin}/cli/context.rs | 7 +- src/bin/cli/main.rs | 22 + src/bin/discord-bot/main.rs | 88 ++++ src/commands/chart.rs | 170 +++++-- src/commands/discord.rs | 151 +++--- src/commands/mod.rs | 4 +- src/commands/score.rs | 83 ++-- src/commands/stats.rs | 119 +++-- src/commands/utils/mod.rs | 34 -- src/context.rs | 82 +++- src/lib.rs | 21 + src/main.rs | 126 ----- src/recognition/fuzzy_song_name.rs | 20 +- src/recognition/recognize.rs | 18 +- src/user.rs | 31 +- 25 files changed, 978 insertions(+), 681 deletions(-) rename src/{cli/mod.rs => bin/cli/command.rs} (63%) rename src/{cli => bin/cli/commands}/analyse.rs (67%) create mode 100644 src/bin/cli/commands/mod.rs rename src/{cli => bin/cli/commands}/prepare_jackets.rs (93%) rename src/{ => bin}/cli/context.rs (91%) create mode 100644 src/bin/cli/main.rs create mode 100644 src/bin/discord-bot/main.rs create mode 100644 src/lib.rs delete mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 53c275f..a9880cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] @@ -33,6 +33,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.8.11" @@ -126,9 +132,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "approx" @@ -153,27 +159,27 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" dependencies = [ "serde", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -207,19 +213,25 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.0", "object", "rustc-demangle", + "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -252,9 +264,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitstream-io" -version = "2.4.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415f8399438eb5e4b2f73ed3152a3448b98149dda642a957ee704e1daa5cf1d8" +checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" [[package]] name = "block-buffer" @@ -267,9 +279,9 @@ dependencies = [ [[package]] name = "built" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a6c0b39c38fd754ac338b00a88066436389c0f029da5d37d1e01091d9b7c17" +checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" [[package]] name = "bumpalo" @@ -285,9 +297,9 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "bytemuck" -version = "1.16.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" [[package]] name = "byteorder" @@ -303,15 +315,15 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "camino" -version = "1.1.7" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ "serde", ] @@ -340,9 +352,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.13" +version = "1.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" dependencies = [ "jobserver", "libc", @@ -382,9 +394,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" dependencies = [ "clap_builder", "clap_derive", @@ -392,9 +404,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ "anstream", "anstyle", @@ -404,14 +416,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -450,9 +462,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" @@ -492,9 +504,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -570,9 +582,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -580,27 +592,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -686,9 +698,9 @@ dependencies = [ [[package]] name = "dwrote" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" +checksum = "2da3498378ed373237bdef1eddcc64e7be2d3ba4841f4c22a998e81cadeea83c" dependencies = [ "lazy_static", "libc", @@ -698,9 +710,9 @@ dependencies = [ [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "embedded-io" @@ -758,7 +770,7 @@ dependencies = [ "flume", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.7.4", "rayon-core", "smallvec", "zune-inflate", @@ -778,9 +790,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fdeflate" @@ -793,12 +805,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -865,7 +877,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -949,7 +961,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -1036,9 +1048,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "glob" @@ -1058,7 +1070,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -1164,9 +1176,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -1212,9 +1224,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1288,12 +1300,12 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d730b085583c4d789dfd07fdcf185be59501666a90c97c40162b37e4fdad272d" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" dependencies = [ "byteorder-lite", - "thiserror", + "quick-error", ] [[package]] @@ -1352,9 +1364,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -1369,14 +1381,14 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "is_terminal_polyfill" @@ -1401,9 +1413,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] @@ -1416,18 +1428,18 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lebe" @@ -1437,9 +1449,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libfuzzer-sys" @@ -1454,9 +1466,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", "windows-targets 0.52.6", @@ -1507,9 +1519,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "loop9" @@ -1537,7 +1549,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", - "rayon", ] [[package]] @@ -1554,9 +1565,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", @@ -1594,14 +1605,24 @@ dependencies = [ ] [[package]] -name = "mio" -version = "0.8.11" +name = "miniz_oxide" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1657,9 +1678,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", @@ -1688,7 +1709,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -1732,21 +1753,11 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" -version = "0.36.0" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -1819,9 +1830,9 @@ dependencies = [ [[package]] name = "pathfinder_simd" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebf45976c56919841273f2a0fc684c28437e2f304e264557d9c72be5d5a718be" +checksum = "5cf07ef4804cfa9aea3b04a7bbdd5a40031dbb6b4f2cbaf2b011666c80c5b4f2" dependencies = [ "rustc_version", ] @@ -1846,9 +1857,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plotters" @@ -1902,7 +1913,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.4", ] [[package]] @@ -1931,7 +1942,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -1954,15 +1965,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1983,7 +1997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -2014,9 +2028,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -2120,16 +2134,15 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.7" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67376f469e7e7840d0040bbf4b9b3334005bb167f814621326e4c7ab8cd6e944" +checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" dependencies = [ "avif-serialize", "imgref", "loop9", "quick-error", "rav1e", - "rayon", "rgb", ] @@ -2161,18 +2174,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", @@ -2181,9 +2194,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -2254,9 +2267,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.37" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] @@ -2310,18 +2323,18 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.6.0", "errno", @@ -2351,7 +2364,7 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] @@ -2367,9 +2380,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" @@ -2383,9 +2396,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", @@ -2462,40 +2475,41 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_cow" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e84ce5596a72f0c4c60759a10ff8c22d5eaf227b0dc2789c8746193309058b" +checksum = "1e7bbbec7196bfde255ab54b65e34087c0849629280028238e67ee25d6a4b7da" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -2531,7 +2545,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_derive", "serde_json", @@ -2548,7 +2562,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -2595,11 +2609,23 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shimmeringmoon" version = "0.1.0" dependencies = [ "anyhow", + "base16ct", "chrono", "clap", "freetype-rs", @@ -2617,6 +2643,7 @@ dependencies = [ "rusqlite_migration", "serde", "serde_with", + "sha2", "tempfile", "tokio", "toml", @@ -2713,9 +2740,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -2730,9 +2757,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -2797,9 +2824,9 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "target-lexicon" -version = "0.12.14" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" @@ -2824,22 +2851,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -2886,9 +2913,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -2901,30 +2928,29 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -2961,14 +2987,14 @@ dependencies = [ "tokio", "tokio-rustls 0.25.0", "tungstenite", - "webpki-roots 0.26.3", + "webpki-roots 0.26.6", ] [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -3000,11 +3026,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", @@ -3013,9 +3039,9 @@ dependencies = [ [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -3037,7 +3063,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -3108,9 +3134,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "typesize" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb704842c709bc76f63e99e704cb208beeccca2abbabd0d9aec02e48ca1cee0f" +checksum = "5dece5c06268af6a9ff4541788601e560a4284ffebfb357f713d676f13b964db" dependencies = [ "chrono", "dashmap", @@ -3132,7 +3158,7 @@ checksum = "905e88c2a4cc27686bd57e495121d451f027e441388a67f773be729ad4be1ea8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -3152,15 +3178,15 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] @@ -3230,9 +3256,9 @@ checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -3261,34 +3287,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", @@ -3298,9 +3325,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3308,22 +3335,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wasm-streams" @@ -3340,9 +3367,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -3356,9 +3383,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" dependencies = [ "rustls-pki-types", ] @@ -3371,9 +3398,9 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "wide" -version = "0.7.26" +version = "0.7.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901e8597c777fa042e9e245bd56c0dc4418c5db3f845b6ff94fbac732c6a0692" +checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690" dependencies = [ "bytemuck", "safe_arch", @@ -3397,11 +3424,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3609,22 +3636,23 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -3650,9 +3678,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 84408a3..e2da31f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,19 @@ name = "shimmeringmoon" version = "0.1.0" edition = "2021" +autobins = false + +[lib] +name = "shimmeringmoon" +path = "src/lib.rs" + +[[bin]] +name = "shimmeringmoon-discord-bot" +path = "src/bin/discord-bot/main.rs" + +[[bin]] +name = "shimmeringmoon-cli" +path = "src/bin/cli/main.rs" [dependencies] chrono = "0.4.38" @@ -25,6 +38,8 @@ clap = { version = "4.5.17", features = ["derive"] } postcard = { version = "1.0.10", features = ["use-std"], default-features = false } serde_with = "3.9.0" anyhow = "1.0.87" +sha2 = "0.10.8" +base16ct = { version = "0.2.0", features = ["alloc"] } -[profile.dev.package."*"] -opt-level = 3 +# [profile.dev.package."*"] +# opt-level = 3 diff --git a/flake.lock b/flake.lock index 625033f..210a78e 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1717827974, - "narHash": "sha256-ixopuTeTouxqTxfMuzs6IaRttbT8JqRW5C9Q/57WxQw=", + "lastModified": 1727073227, + "narHash": "sha256-1kmkEQmFfGVuPBasqSZrNThqyMDV1SzTalQdRZxtDRs=", "owner": "nix-community", "repo": "fenix", - "rev": "ab655c627777ab5f9964652fe23bbb1dfbd687a8", + "rev": "88cc292eb3c689073c784d6aecc0edbd47e12881", "type": "github" }, "original": { @@ -41,16 +41,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1718000748, - "narHash": "sha256-zliqz7ovpxYdKIK+GlWJZxifXsT9A1CHNQhLxV0G1Hc=", + "lastModified": 1726755586, + "narHash": "sha256-PmUr/2GQGvFTIJ6/Tvsins7Q43KTMvMFhvG6oaYK+Wk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "869cab745a802b693b45d193b460c9184da671f3", + "rev": "c04d5652cfa9742b1d519688f65d1bbccea9eb7e", "type": "github" }, "original": { "owner": "nixos", - "ref": "release-24.05", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } @@ -59,17 +59,18 @@ "inputs": { "fenix": "fenix", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" } }, "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1717583671, - "narHash": "sha256-+lRAmz92CNUxorqWusgJbL9VE1eKCnQQojglRemzwkw=", + "lastModified": 1726443025, + "narHash": "sha256-nCmG4NJpwI0IoIlYlwtDwVA49yuspA2E6OhfCOmiArQ=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "48bbdd6a74f3176987d5c809894ac33957000d19", + "rev": "94b526fc86eaa0e90fb4d54a5ba6313aa1e9b269", "type": "github" }, "original": { @@ -79,6 +80,26 @@ "type": "github" } }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1727058553, + "narHash": "sha256-tY/UU3Qk5gP/J0uUM4DZ6wo4arNLGAVqLKBotILykfQ=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "edc5b0f896170f07bd39ad59d6186fcc7859bbb2", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, "systems": { "locked": { "lastModified": 1681028828, diff --git a/flake.nix b/flake.nix index a2d34a2..698aa8b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,17 +1,22 @@ { inputs = { - nixpkgs.url = "github:nixos/nixpkgs/release-24.05"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; fenix.url = "github:nix-community/fenix"; fenix.inputs.nixpkgs.follows = "nixpkgs"; + rust-overlay.url = "github:oxalica/rust-overlay"; + rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = - { ... }@inputs: + inputs: inputs.flake-utils.lib.eachSystem (with inputs.flake-utils.lib.system; [ x86_64-linux ]) ( system: let - pkgs = inputs.nixpkgs.legacyPackages.${system}.extend inputs.fenix.overlays.default; + pkgs = inputs.nixpkgs.legacyPackages.${system}.extend (import inputs.rust-overlay); + # toolchain = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default); + # toolchain = pkgs.rust-bin.stable.latest.default; + toolchain = inputs.fenix.packages.${system}.complete.toolchain; inherit (pkgs) lib; in { @@ -29,41 +34,35 @@ }; }; }; - devShell = pkgs.mkShell rec { - packages = with pkgs; [ - (fenix.complete.withComponents [ - "cargo" - "clippy" - "rust-src" - "rustc" - "rustfmt" - ]) - rust-analyzer-nightly - ruff - imagemagick - fontconfig - freetype - - clang - llvmPackages.clang + devShell = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + toolchain + # ruff + # imagemagick pkg-config + # clang + # llvmPackages.clang + ]; + buildInputs = with pkgs; [ + toolchain + freetype + fontconfig leptonica tesseract - openssl + # openssl sqlite ]; - LD_LIBRARY_PATH = lib.makeLibraryPath packages; + # LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; # compilation of -sys packages requires manually setting LIBCLANG_PATH - LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + # LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; }; } ); # {{{ Caching and whatnot - # TODO: persist trusted substituters file nixConfig = { extra-substituters = [ "https://nix-community.cachix.org" ]; diff --git a/src/arcaea/achievement.rs b/src/arcaea/achievement.rs index 5e2881a..7aaed1d 100644 --- a/src/arcaea/achievement.rs +++ b/src/arcaea/achievement.rs @@ -3,7 +3,7 @@ use anyhow::anyhow; use image::RgbaImage; use crate::assets::get_data_dir; -use crate::context::{Error, UserContext}; +use crate::context::{ErrorKind, TagError, TaggedError, UserContext}; use crate::user::User; use super::chart::{Difficulty, Level}; @@ -119,9 +119,8 @@ impl GoalStats { ctx: &UserContext, user: &User, scoring_system: ScoringSystem, - ) -> Result { - let plays = get_best_plays(ctx, user.id, scoring_system, 0, usize::MAX, None)? - .map_err(|s| anyhow!("{s}"))?; + ) -> Result { + let plays = get_best_plays(ctx, user.id, scoring_system, 0, usize::MAX, None)?; let conn = ctx.db.get()?; // {{{ PM count @@ -141,14 +140,14 @@ impl GoalStats { let peak_ptt = conn .prepare_cached( " - SELECT s.creation_ptt - FROM plays p - JOIN scores s ON s.play_id = p.id - WHERE user_id = ? - AND scoring_system = ? - ORDER BY s.creation_ptt DESC - LIMIT 1 - ", + SELECT s.creation_ptt + FROM plays p + JOIN scores s ON s.play_id = p.id + WHERE user_id = ? + AND scoring_system = ? + ORDER BY s.creation_ptt DESC + LIMIT 1 + ", )? .query_row( ( @@ -157,7 +156,7 @@ impl GoalStats { ), |row| row.get(0), ) - .map_err(|_| anyhow!("No ptt history data found"))?; + .map_err(|_| anyhow!("No ptt history data found").tag(ErrorKind::User))?; // }}} // {{{ Peak PM relay let peak_pm_relay = { @@ -309,6 +308,7 @@ impl Default for AchievementTowers { ]); // }}} // {{{ PTT tower + #[allow(clippy::zero_prefixed_literal)] let ptt_tower = AchievementTower::new(vec![ Achievement::new(PTT(0800)), Achievement::new(PTT(0900)), diff --git a/src/arcaea/play.rs b/src/arcaea/play.rs index 90cdda9..b2bafe4 100644 --- a/src/arcaea/play.rs +++ b/src/arcaea/play.rs @@ -2,6 +2,7 @@ use std::array; use std::num::NonZeroU64; +use anyhow::anyhow; use anyhow::Context; use chrono::NaiveDateTime; use chrono::Utc; @@ -13,6 +14,9 @@ use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateEmbedAuthor, use rusqlite::Row; use crate::arcaea::chart::{Chart, Song}; +use crate::context::ErrorKind; +use crate::context::TagError; +use crate::context::TaggedError; use crate::context::{Error, UserContext}; use crate::user::User; @@ -61,7 +65,7 @@ impl CreatePlay { } // {{{ Save - pub fn save(self, ctx: &UserContext, user: &User, chart: &Chart) -> Result { + pub fn save(self, ctx: &UserContext, user: &User, chart: &Chart) -> Result { let conn = ctx.db.get()?; let attachment_id = self.discord_attachment_id.map(|i| i.get() as i64); @@ -104,9 +108,7 @@ impl CreatePlay { for system in ScoringSystem::SCORING_SYSTEMS { let i = system.to_index(); - let plays = get_best_plays(ctx, user.id, system, 30, 30, None)?.ok(); - - let creation_ptt: Option<_> = try { rating_as_fixed(compute_b30_ptt(system, &plays?)) }; + let creation_ptt = try_compute_ptt(ctx, user.id, system, None)?; conn.prepare_cached( " @@ -321,10 +323,9 @@ impl Play { self.score(ScoringSystem::Standard).0, index ); - let icon_attachement = match chart.cached_jacket.as_ref() { - Some(jacket) => Some(CreateAttachment::bytes(jacket.raw, &attachement_name)), - None => None, - }; + let icon_attachement = chart + .cached_jacket + .map(|jacket| CreateAttachment::bytes(jacket.raw, &attachement_name)); let mut embed = CreateEmbed::default() .title(format!( @@ -378,7 +379,7 @@ impl Play { if let Some(max_recall) = self.max_recall { format!("{}", max_recall) } else { - format!("-") + "-".to_string() }, true, ) @@ -409,14 +410,14 @@ impl Play { // {{{ General functions pub type PlayCollection<'a> = Vec<(Play, &'a Song, &'a Chart)>; -pub fn get_best_plays<'a>( - ctx: &'a UserContext, +pub fn get_best_plays( + ctx: &UserContext, user_id: u32, scoring_system: ScoringSystem, min_amount: usize, max_amount: usize, before: Option, -) -> Result, String>, Error> { +) -> Result, TaggedError> { let conn = ctx.db.get()?; // {{{ DB data fetching let mut plays = conn @@ -453,10 +454,11 @@ pub fn get_best_plays<'a>( // }}} if plays.len() < min_amount { - return Ok(Err(format!( + return Err(anyhow!( "Not enough plays found ({} out of a minimum of {min_amount})", plays.len() - ))); + ) + .tag(crate::context::ErrorKind::User)); } // {{{ B30 computation @@ -464,7 +466,27 @@ pub fn get_best_plays<'a>( plays.truncate(max_amount); // }}} - Ok(Ok(plays)) + Ok(plays) +} + +/// Compute the current ptt of a given user. +/// +/// This is similar to directly calling [get_best_plays] and then passing the +/// result into [compute_b30_ptt], except any user errors (i.e.: not enough +/// plays available) get turned into [None] values. +pub fn try_compute_ptt( + ctx: &UserContext, + user_id: u32, + system: ScoringSystem, + before: Option, +) -> Result, Error> { + match get_best_plays(ctx, user_id, system, 30, 30, before) { + Err(err) => match err.kind { + ErrorKind::User => Ok(None), + ErrorKind::Internal => Err(err.error), + }, + Ok(plays) => Ok(Some(rating_as_fixed(compute_b30_ptt(system, &plays)))), + } } #[inline] @@ -478,7 +500,7 @@ pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_> } // }}} // {{{ Maintenance functions -pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), Error> { +pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), TaggedError> { let conn = ctx.db.get()?; let mut query = conn.prepare_cached( " @@ -504,10 +526,8 @@ pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), Error> { let play = play?; for system in ScoringSystem::SCORING_SYSTEMS { let i = system.to_index(); - let plays = - get_best_plays(&ctx, play.user_id, system, 30, 30, Some(play.created_at))?.ok(); + let creation_ptt = try_compute_ptt(ctx, play.user_id, system, Some(play.created_at))?; - let creation_ptt: Option<_> = try { rating_as_fixed(compute_b30_ptt(system, &plays?)) }; let raw_score = play.scores.0[i].0; conn.prepare_cached( diff --git a/src/cli/mod.rs b/src/bin/cli/command.rs similarity index 63% rename from src/cli/mod.rs rename to src/bin/cli/command.rs index db9d9e4..044df48 100644 --- a/src/cli/mod.rs +++ b/src/bin/cli/command.rs @@ -1,7 +1,3 @@ -pub mod analyse; -pub mod context; -pub mod prepare_jackets; - #[derive(clap::Parser)] #[command(author, version, about, long_about = None)] pub struct Cli { @@ -11,8 +7,6 @@ pub struct Cli { #[derive(clap::Subcommand)] pub enum Command { - /// Start the discord bot - Discord {}, PrepareJackets {}, - Analyse(analyse::Args), + Analyse(crate::commands::analyse::Args), } diff --git a/src/cli/analyse.rs b/src/bin/cli/commands/analyse.rs similarity index 67% rename from src/cli/analyse.rs rename to src/bin/cli/commands/analyse.rs index c0dc2e6..75ba52d 100644 --- a/src/cli/analyse.rs +++ b/src/bin/cli/commands/analyse.rs @@ -1,9 +1,9 @@ // {{{ Imports use std::path::PathBuf; -use crate::cli::context::CliContext; -use crate::commands::score::magic_impl; -use crate::context::{Error, UserContext}; +use crate::context::CliContext; +use shimmeringmoon::commands::score::magic_impl; +use shimmeringmoon::context::{Error, UserContext}; // }}} #[derive(clap::Args)] diff --git a/src/bin/cli/commands/mod.rs b/src/bin/cli/commands/mod.rs new file mode 100644 index 0000000..61c28e2 --- /dev/null +++ b/src/bin/cli/commands/mod.rs @@ -0,0 +1,2 @@ +pub mod analyse; +pub mod prepare_jackets; diff --git a/src/cli/prepare_jackets.rs b/src/bin/cli/commands/prepare_jackets.rs similarity index 93% rename from src/cli/prepare_jackets.rs rename to src/bin/cli/commands/prepare_jackets.rs index 50a99fa..771aed3 100644 --- a/src/cli/prepare_jackets.rs +++ b/src/bin/cli/commands/prepare_jackets.rs @@ -5,11 +5,11 @@ use std::io::{stdout, Write}; use anyhow::{anyhow, bail, Context}; use image::imageops::FilterType; -use crate::arcaea::chart::{Difficulty, SongCache}; -use crate::arcaea::jacket::{ImageVec, BITMAP_IMAGE_SIZE}; -use crate::assets::{get_asset_dir, get_data_dir}; -use crate::context::{connect_db, Error}; -use crate::recognition::fuzzy_song_name::guess_chart_name; +use shimmeringmoon::arcaea::chart::{Difficulty, SongCache}; +use shimmeringmoon::arcaea::jacket::{ImageVec, BITMAP_IMAGE_SIZE}; +use shimmeringmoon::assets::{get_asset_dir, get_data_dir}; +use shimmeringmoon::context::{connect_db, Error}; +use shimmeringmoon::recognition::fuzzy_song_name::guess_chart_name; // }}} /// Hacky function which clears the current line of the standard output. diff --git a/src/cli/context.rs b/src/bin/cli/context.rs similarity index 91% rename from src/cli/context.rs rename to src/bin/cli/context.rs index 5c1514d..fff301a 100644 --- a/src/cli/context.rs +++ b/src/bin/cli/context.rs @@ -5,9 +5,10 @@ use std::str::FromStr; use poise::serenity_prelude::{CreateAttachment, CreateMessage}; -use crate::assets::get_var; -use crate::context::Error; -use crate::{commands::discord::MessageContext, context::UserContext}; +extern crate shimmeringmoon; +use shimmeringmoon::assets::get_var; +use shimmeringmoon::context::Error; +use shimmeringmoon::{commands::discord::MessageContext, context::UserContext}; // }}} /// Similar in scope to [crate::commands::discord::mock::MockContext], diff --git a/src/bin/cli/main.rs b/src/bin/cli/main.rs new file mode 100644 index 0000000..6be8dd0 --- /dev/null +++ b/src/bin/cli/main.rs @@ -0,0 +1,22 @@ +use clap::Parser; +use command::{Cli, Command}; +use shimmeringmoon::context::{Error, UserContext}; + +mod command; +mod commands; +mod context; + +#[tokio::main] +async fn main() -> Result<(), Error> { + let cli = Cli::parse(); + match cli.command { + Command::PrepareJackets {} => { + commands::prepare_jackets::run()?; + } + Command::Analyse(args) => { + commands::analyse::run(args).await?; + } + } + + Ok(()) +} diff --git a/src/bin/discord-bot/main.rs b/src/bin/discord-bot/main.rs new file mode 100644 index 0000000..74a8797 --- /dev/null +++ b/src/bin/discord-bot/main.rs @@ -0,0 +1,88 @@ +use poise::serenity_prelude::{self as serenity}; +extern crate shimmeringmoon; +use shimmeringmoon::arcaea::play::generate_missing_scores; +use shimmeringmoon::context::{Error, UserContext}; +use shimmeringmoon::{commands, timed}; +use std::{env::var, sync::Arc, time::Duration}; + +// {{{ Error handler +async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) { + match error { + error => { + if let Err(e) = poise::builtins::on_error(error).await { + println!("Error while handling error: {}", e) + } + } + } +} +// }}} + +#[tokio::main] +async fn main() { + // {{{ Poise options + let options = poise::FrameworkOptions { + commands: vec![ + commands::help(), + commands::score::score(), + commands::stats::stats(), + commands::chart::chart(), + ], + prefix_options: poise::PrefixFrameworkOptions { + stripped_dynamic_prefix: Some(|_ctx, message, _user_ctx| { + Box::pin(async { + if message.author.bot || Into::::into(message.author.id) == 1 { + Ok(None) + } else if message.content.starts_with("!") { + Ok(Some(message.content.split_at(1))) + } else if message.guild_id.is_none() { + if message.content.trim().len() == 0 { + Ok(Some(("", "score magic"))) + } else { + Ok(Some(("", &message.content[..]))) + } + } else { + Ok(None) + } + }) + }), + edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan( + Duration::from_secs(3600), + ))), + ..Default::default() + }, + on_error: |error| Box::pin(on_error(error)), + ..Default::default() + }; + // }}} + // {{{ Start poise + let framework = poise::Framework::builder() + .setup(move |ctx, _ready, framework| { + Box::pin(async move { + println!("Logged in as {}", _ready.user.name); + poise::builtins::register_globally(ctx, &framework.options().commands).await?; + let ctx = UserContext::new().await?; + + if var("SHIMMERING_REGEN_SCORES").unwrap_or_default() == "1" { + timed!("generate_missing_scores", { + generate_missing_scores(&ctx).await?; + }); + } + + Ok(ctx) + }) + }) + .options(options) + .build(); + + let token = + var("SHIMMERING_DISCORD_TOKEN").expect("Missing `SHIMMERING_DISCORD_TOKEN` env var"); + let intents = + serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT; + + let client = serenity::ClientBuilder::new(token, intents) + .framework(framework) + .await; + + client.unwrap().start().await.unwrap() + // }}} +} diff --git a/src/commands/chart.rs b/src/commands/chart.rs index d2bd79d..7801247 100644 --- a/src/commands/chart.rs +++ b/src/commands/chart.rs @@ -1,11 +1,11 @@ -// {{{ Imports use anyhow::anyhow; -use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage}; +// {{{ Imports +use poise::serenity_prelude::{CreateAttachment, CreateEmbed}; use crate::arcaea::{chart::Side, play::Play}; -use crate::context::{Context, Error}; -use crate::get_user; +use crate::context::{Context, Error, ErrorKind, TagError, TaggedError}; use crate::recognition::fuzzy_song_name::guess_song_and_chart; +use crate::user::User; use std::io::Cursor; use chrono::DateTime; @@ -20,7 +20,7 @@ use poise::CreateReply; use crate::arcaea::score::{Score, ScoringSystem}; -use super::discord::MessageContext; +use super::discord::{CreateReplyExtra, MessageContext}; // }}} // {{{ Top command @@ -37,14 +37,13 @@ pub async fn chart(_ctx: Context<'_>) -> Result<(), Error> { // }}} // {{{ Info // {{{ Implementation -async fn info_impl(ctx: &mut impl MessageContext, name: &str) -> Result<(), Error> { - let (song, chart) = guess_song_and_chart(&ctx.data(), name)?; +async fn info_impl(ctx: &mut impl MessageContext, name: &str) -> Result<(), TaggedError> { + let (song, chart) = guess_song_and_chart(ctx.data(), name)?; let attachement_name = "chart.png"; - let icon_attachement = match chart.cached_jacket.as_ref() { - Some(jacket) => Some(CreateAttachment::bytes(jacket.raw, attachement_name)), - None => None, - }; + let icon_attachement = chart + .cached_jacket + .map(|jacket| CreateAttachment::bytes(jacket.raw, attachement_name)); let play_count: usize = ctx .data() @@ -57,7 +56,8 @@ async fn info_impl(ctx: &mut impl MessageContext, name: &str) -> Result<(), Erro WHERE chart_id=? ", )? - .query_row([chart.id], |row| row.get(0))?; + .query_row([chart.id], |row| row.get(0)) + .unwrap_or(0); let mut embed = CreateEmbed::default() .title(format!( @@ -87,8 +87,13 @@ async fn info_impl(ctx: &mut impl MessageContext, name: &str) -> Result<(), Erro embed = embed.thumbnail(format!("attachment://{}", &attachement_name)); } - ctx.send_files(icon_attachement, CreateMessage::new().embed(embed)) - .await?; + ctx.send( + CreateReply::default() + .reply(true) + .embed(embed) + .attachments(icon_attachement), + ) + .await?; Ok(()) } @@ -138,24 +143,19 @@ async fn info( #[description = "Name of chart to show (difficulty at the end)"] name: String, ) -> Result<(), Error> { - info_impl(&mut ctx, &name).await?; + let res = info_impl(&mut ctx, &name).await; + ctx.handle_error(res).await?; Ok(()) } // }}} // }}} // {{{ Best score -/// Show the best score on a given chart -#[poise::command(prefix_command, slash_command, user_cooldown = 1)] -async fn best( - mut ctx: Context<'_>, - #[rest] - #[description = "Name of chart to show (difficulty at the end)"] - name: String, -) -> Result<(), Error> { - let user = get_user!(&mut ctx); +// {{{ Implementation +async fn best_impl(ctx: &mut C, name: &str) -> Result { + let user = User::from_context(ctx)?; - let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?; + let (song, chart) = guess_song_and_chart(ctx.data(), name)?; let play = ctx .data() .db @@ -181,6 +181,7 @@ async fn best( song.title, chart.difficulty ) + .tag(ErrorKind::User) })?; let (embed, attachment) = play.to_embed( @@ -192,27 +193,91 @@ async fn best( Some(&ctx.fetch_user(&user.discord_id).await?), )?; - ctx.channel_id() - .send_files(ctx.http(), attachment, CreateMessage::new().embed(embed)) - .await?; + ctx.send( + CreateReply::default() + .reply(true) + .embed(embed) + .attachments(attachment), + ) + .await?; - Ok(()) + Ok(play) } // }}} -// {{{ Score plot +// {{{ Tests +// {{{ Tests +#[cfg(test)] +mod best_tests { + use std::path::PathBuf; + + use crate::{ + commands::{discord::mock::MockContext, score::magic_impl}, + with_test_ctx, + }; + + use super::*; + + #[tokio::test] + async fn no_scores() -> Result<(), Error> { + with_test_ctx!("test/commands/chart/best/specify_difficulty", async |ctx| { + best_impl(ctx, "Pentiment").await?; + Ok(()) + }) + } + + #[tokio::test] + async fn pick_correct_score() -> Result<(), Error> { + with_test_ctx!( + "test/commands/chart/best/last_byd", + async |ctx: &mut MockContext| { + magic_impl( + ctx, + &[ + PathBuf::from_str("test/screenshots/fracture_ray_ex.jpg")?, + // Make sure we aren't considering higher scores from other stuff + PathBuf::from_str("test/screenshots/antithese_74_kerning.jpg")?, + PathBuf::from_str("test/screenshots/fracture_ray_missed_ex.jpg")?, + ], + ) + .await?; + + let play = best_impl(ctx, "Fracture ray").await?; + assert_eq!(play.score(ScoringSystem::Standard).0, 9_805_651); + + Ok(()) + } + ) + } +} +// }}} +// }}} +// {{{ Discord wrapper /// Show the best score on a given chart -#[poise::command(prefix_command, slash_command, user_cooldown = 10)] -async fn plot( +#[poise::command(prefix_command, slash_command, user_cooldown = 1)] +async fn best( mut ctx: Context<'_>, - scoring_system: Option, #[rest] #[description = "Name of chart to show (difficulty at the end)"] name: String, ) -> Result<(), Error> { - let user = get_user!(&mut ctx); + let res = best_impl(&mut ctx, &name).await; + ctx.handle_error(res).await?; + + Ok(()) +} +// }}} +// }}} +// {{{ Score plot +// {{{ Implementation +async fn plot_impl( + ctx: &mut C, + scoring_system: Option, + name: String, +) -> Result<(), TaggedError> { + let user = User::from_context(ctx)?; let scoring_system = scoring_system.unwrap_or_default(); - let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?; + let (song, chart) = guess_song_and_chart(ctx.data(), &name)?; // SAFETY: we limit the amount of plotted plays to 1000. let plays = ctx @@ -236,13 +301,11 @@ async fn plot( .query_map((user.id, chart.id), |row| Play::from_sql(chart, row))? .collect::, _>>()?; - if plays.len() == 0 { - ctx.reply(format!( - "No plays found on {} [{:?}]", - song.title, chart.difficulty - )) - .await?; - return Ok(()); + if plays.is_empty() { + return Err( + anyhow!("No plays found on {} [{:?}]", song.title, chart.difficulty) + .tag(ErrorKind::User), + ); } let min_time = plays.iter().map(|p| p.created_at).min().unwrap(); @@ -255,7 +318,7 @@ async fn plot( .0 as i64; if min_score > 9_900_000 { - min_score = 9_800_000; + min_score = 9_900_000; } else if min_score > 9_800_000 { min_score = 9_800_000; } else if min_score > 9_500_000 { @@ -331,9 +394,28 @@ async fn plot( let mut cursor = Cursor::new(&mut buffer); image.write_to(&mut cursor, image::ImageFormat::Png)?; - let reply = CreateReply::default().attachment(CreateAttachment::bytes(buffer, "plot.png")); + let reply = CreateReply::default() + .reply(true) + .attachment(CreateAttachment::bytes(buffer, "plot.png")); ctx.send(reply).await?; Ok(()) } // }}} +// {{{ Discord wrapper +/// Show the best score on a given chart +#[poise::command(prefix_command, slash_command, user_cooldown = 10)] +async fn plot( + mut ctx: Context<'_>, + scoring_system: Option, + #[rest] + #[description = "Name of chart to show (difficulty at the end)"] + name: String, +) -> Result<(), Error> { + let res = plot_impl(&mut ctx, scoring_system, name).await; + ctx.handle_error(res).await?; + + Ok(()) +} +// }}} +// }}} diff --git a/src/commands/discord.rs b/src/commands/discord.rs index 4c0ba51..cfb69cb 100644 --- a/src/commands/discord.rs +++ b/src/commands/discord.rs @@ -3,10 +3,11 @@ use std::num::NonZeroU64; use std::str::FromStr; use poise::serenity_prelude::futures::future::join_all; -use poise::serenity_prelude::{CreateAttachment, CreateMessage}; +use poise::serenity_prelude::{CreateAttachment, CreateEmbed}; +use poise::CreateReply; use crate::arcaea::play::Play; -use crate::context::{Error, UserContext}; +use crate::context::{Error, ErrorKind, TaggedError, UserContext}; use crate::timed; // }}} @@ -22,17 +23,8 @@ pub trait MessageContext { /// Reply to the current message async fn reply(&mut self, text: &str) -> Result<(), Error>; - /// Deliver a message containing references to files. - async fn send_files( - &mut self, - attachments: impl IntoIterator, - message: CreateMessage, - ) -> Result<(), Error>; - /// Deliver a message - async fn send(&mut self, message: CreateMessage) -> Result<(), Error> { - self.send_files([], message).await - } + async fn send(&mut self, message: CreateReply) -> Result<(), Error>; // {{{ Input attachments type Attachment; @@ -61,6 +53,20 @@ pub trait MessageContext { .collect::>() } // }}} + // {{{ Erorr handling + async fn handle_error(&mut self, res: Result) -> Result, Error> { + match res { + Ok(v) => Ok(Some(v)), + Err(e) => match e.kind { + ErrorKind::Internal => Err(e.error), + ErrorKind::User => { + self.reply(&format!("{}", e.error)).await?; + Ok(None) + } + }, + } + } + // }}} } // }}} // {{{ Poise implementation @@ -87,14 +93,8 @@ impl<'a> MessageContext for poise::Context<'a, UserContext, Error> { Ok(()) } - async fn send_files( - &mut self, - attachments: impl IntoIterator, - message: CreateMessage, - ) -> Result<(), Error> { - self.channel_id() - .send_files(self.http(), attachments, message) - .await?; + async fn send(&mut self, message: CreateReply) -> Result<(), Error> { + poise::send_reply(*self, message).await?; Ok(()) } @@ -122,6 +122,10 @@ impl<'a> MessageContext for poise::Context<'a, UserContext, Error> { pub mod mock { use std::{env, fs, path::PathBuf}; + use poise::serenity_prelude::CreateEmbed; + use serde::{Deserialize, Serialize}; + use sha2::{Digest, Sha256}; + use super::*; /// A mock context usable for testing. Messages and attachments are @@ -130,7 +134,26 @@ pub mod mock { pub struct MockContext { pub user_id: u64, pub data: UserContext, - pub messages: Vec<(CreateMessage, Vec)>, + messages: Vec, + } + + /// Holds test-relevant data about an attachment. + #[derive(Debug, Clone, Serialize, Deserialize)] + struct AttachmentEssence { + filename: String, + description: Option, + /// SHA-256 hash of the file + hash: String, + } + + /// Holds test-relevant data about a reply. + #[derive(Debug, Clone, Serialize)] + struct ReplyEssence { + reply: bool, + ephermal: Option, + content: Option, + embeds: Vec, + attachments: Vec, } impl MockContext { @@ -157,10 +180,8 @@ pub mod mock { } fs::create_dir_all(path)?; - for (i, (message, attachments)) in self.messages.iter().enumerate() { - let dir = path.join(format!("{i}")); - fs::create_dir_all(&dir)?; - let message_file = dir.join("message.toml"); + for (i, message) in self.messages.iter().enumerate() { + let message_file = path.join(format!("{i}.toml")); if message_file.exists() { assert_eq!( @@ -170,28 +191,6 @@ pub mod mock { } else { fs::write(&message_file, toml::to_string_pretty(message)?)?; } - - for attachment in attachments { - let path = dir.join(&attachment.filename); - - if path.exists() { - if &attachment.data != &fs::read(&path)? { - panic!("Attachment differs from {path:?}"); - } - } else { - fs::write(&path, &attachment.data)?; - } - } - - // Ensure there's no extra attachments on disk - let file_count = fs::read_dir(dir)?.count(); - if file_count != attachments.len() + 1 { - panic!( - "Only {} attachments found instead of {}", - attachments.len(), - file_count - 1 - ); - } } Ok(()) @@ -219,18 +218,33 @@ pub mod mock { } async fn reply(&mut self, text: &str) -> Result<(), Error> { - self.messages - .push((CreateMessage::new().content(text), Vec::new())); - Ok(()) + self.send(CreateReply::default().content(text).reply(true)) + .await } - async fn send_files( - &mut self, - attachments: impl IntoIterator, - message: CreateMessage, - ) -> Result<(), Error> { - self.messages - .push((message, attachments.into_iter().collect())); + async fn send(&mut self, message: CreateReply) -> Result<(), Error> { + self.messages.push(ReplyEssence { + reply: message.reply, + ephermal: message.ephemeral, + content: message.content, + embeds: message.embeds, + attachments: message + .attachments + .into_iter() + .map(|attachment| AttachmentEssence { + filename: attachment.filename, + description: attachment.description, + hash: { + let hash = Sha256::digest(&attachment.data); + let string = base16ct::lower::encode_string(&hash); + + // We allocate twice, but it's only at the end of tests, + // so it should be fineeeeeeee + format!("sha256_{string}") + }, + }) + .collect(), + }); Ok(()) } @@ -265,4 +279,27 @@ pub mod mock { pub fn play_song_title<'a>(ctx: &'a impl MessageContext, play: &'a Play) -> Result<&'a str, Error> { Ok(&ctx.data().song_cache.lookup_chart(play.chart_id)?.0.title) } + +pub trait CreateReplyExtra { + fn attachments(self, attachments: impl IntoIterator) -> Self; + fn embeds(self, embeds: impl IntoIterator) -> Self; +} + +impl CreateReplyExtra for CreateReply { + fn attachments(mut self, attachments: impl IntoIterator) -> Self { + for attachment in attachments.into_iter() { + self = self.attachment(attachment); + } + + self + } + + fn embeds(mut self, embeds: impl IntoIterator) -> Self { + for embed in embeds.into_iter() { + self = self.embed(embed); + } + + self + } +} // }}} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 58f8644..0a49309 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -33,7 +33,7 @@ pub async fn help( /// Explains the different scoring systems #[poise::command(prefix_command, slash_command)] async fn scoring(ctx: Context<'_>) -> Result<(), Error> { - static CONTENT: &'static str = " + static CONTENT: &str = " ## 1. Standard scoring (`standard`): This is the base-game Arcaea scoring system we all know and love! Points are awarded for each note, with a `2:1` pure:far ratio. The score is then scaled up such that `10_000_000` is the maximum. Last but not least, the number of max pures is added to the total. @@ -58,7 +58,7 @@ Most commands take an optional parameter specifying what scoring system to use. /// Explains the different scoring systems using gen-z slang #[poise::command(prefix_command, slash_command)] async fn scoringz(ctx: Context<'_>) -> Result<(), Error> { - static CONTENT: &'static str = " + static CONTENT: &str = " ## 1. Standard scoring (`standard`): Alright, fam, this is the OG Arcaea scoring setup that everyone vibes with! You hit notes, you get points — easy clap. The ratio is straight up `2:1` pure:far. The score then gets a glow-up, maxing out at `10 milly`. And hold up, you even get bonus points for those max pures at the end. No cap, this is the classic way to flex your skills. diff --git a/src/commands/score.rs b/src/commands/score.rs index 1edff2b..6500307 100644 --- a/src/commands/score.rs +++ b/src/commands/score.rs @@ -1,16 +1,15 @@ // {{{ Imports use crate::arcaea::play::{CreatePlay, Play}; use crate::arcaea::score::Score; -use crate::context::{Context, Error}; +use crate::context::{Context, Error, ErrorKind, TagError, TaggedError}; use crate::recognition::recognize::{ImageAnalyzer, ScoreKind}; use crate::user::User; -use crate::{get_user, timed}; +use crate::{get_user_error, timed}; use anyhow::anyhow; use image::DynamicImage; -use poise::serenity_prelude as serenity; -use poise::serenity_prelude::CreateMessage; +use poise::{serenity_prelude as serenity, CreateReply}; -use super::discord::MessageContext; +use super::discord::{CreateReplyExtra, MessageContext}; // }}} // {{{ Score @@ -30,13 +29,12 @@ pub async fn score(_ctx: Context<'_>) -> Result<(), Error> { pub async fn magic_impl( ctx: &mut C, files: &[C::Attachment], -) -> Result, Error> { - let user = get_user!(ctx); - let files = ctx.download_images(&files).await?; +) -> Result, TaggedError> { + let user = User::from_context(ctx)?; + let files = ctx.download_images(files).await?; - if files.len() == 0 { - ctx.reply("No images found attached to message").await?; - return Ok(vec![]); + if files.is_empty() { + return Err(anyhow!("No images found attached to message").tag(ErrorKind::User)); } let mut embeds = Vec::with_capacity(files.len()); @@ -50,7 +48,7 @@ pub async fn magic_impl( let mut grayscale_image = DynamicImage::ImageLuma8(image.to_luma8()); // }}} - let result: Result<(), Error> = try { + let result: Result<(), TaggedError> = try { // {{{ Detection let kind = timed!("read_score_kind", { @@ -102,13 +100,13 @@ pub async fn magic_impl( .with_attachment(C::attachment_id(attachment)) .with_fars(maybe_fars) .with_max_recall(max_recall) - .save(&ctx.data(), &user, &chart)?; + .save(ctx.data(), &user, chart)?; // }}} // }}} // {{{ Deliver embed let (embed, attachment) = timed!("to embed", { - play.to_embed(ctx.data(), &user, &song, &chart, i, None)? + play.to_embed(ctx.data(), &user, song, chart, i, None)? }); plays.push(play); @@ -118,15 +116,21 @@ pub async fn magic_impl( }; if let Err(err) = result { + let user_err = get_user_error!(err); analyzer - .send_discord_error(ctx, &image, C::filename(&attachment), err) + .send_discord_error(ctx, &image, C::filename(attachment), user_err) .await?; } } - if embeds.len() > 0 { - ctx.send_files(attachments, CreateMessage::new().embeds(embeds)) - .await?; + if !embeds.is_empty() { + ctx.send( + CreateReply::default() + .reply(true) + .embeds(embeds) + .attachments(attachments), + ) + .await?; } Ok(plays) @@ -203,7 +207,8 @@ pub async fn magic( mut ctx: Context<'_>, #[description = "Images containing scores"] files: Vec, ) -> Result<(), Error> { - magic_impl(&mut ctx, &files).await?; + let res = magic_impl(&mut ctx, &files).await; + ctx.handle_error(res).await?; Ok(()) } @@ -211,10 +216,12 @@ pub async fn magic( // }}} // {{{ Score show // {{{ Implementation -pub async fn show_impl(ctx: &mut C, ids: &[u32]) -> Result, Error> { - if ids.len() == 0 { - ctx.reply("Empty ID list provided").await?; - return Ok(vec![]); +pub async fn show_impl( + ctx: &mut C, + ids: &[u32], +) -> Result, TaggedError> { + if ids.is_empty() { + return Err(anyhow!("Empty ID list provided").tag(ErrorKind::User)); } let mut embeds = Vec::with_capacity(ids.len()); @@ -225,7 +232,7 @@ pub async fn show_impl(ctx: &mut C, ids: &[u32]) -> Result(ctx: &mut C, ids: &[u32]) -> Result { ctx.send( - CreateMessage::new().content(format!("Could not find play with id {}", id)), + CreateReply::default().content(format!("Could not find play with id {}", id)), ) .await?; continue; @@ -269,9 +276,14 @@ pub async fn show_impl(ctx: &mut C, ids: &[u32]) -> Result 0 { - ctx.send_files(attachments, CreateMessage::new().embeds(embeds)) - .await?; + if !embeds.is_empty() { + ctx.send( + CreateReply::default() + .reply(true) + .embeds(embeds) + .attachments(attachments), + ) + .await?; } Ok(plays) @@ -333,7 +345,8 @@ pub async fn show( mut ctx: Context<'_>, #[description = "Ids of score to show"] ids: Vec, ) -> Result<(), Error> { - show_impl(&mut ctx, &ids).await?; + let res = show_impl(&mut ctx, &ids).await; + ctx.handle_error(res).await?; Ok(()) } @@ -341,12 +354,11 @@ pub async fn show( // }}} // {{{ Score delete // {{{ Implementation -pub async fn delete_impl(ctx: &mut C, ids: &[u32]) -> Result<(), Error> { - let user = get_user!(ctx); +pub async fn delete_impl(ctx: &mut C, ids: &[u32]) -> Result<(), TaggedError> { + let user = User::from_context(ctx)?; - if ids.len() == 0 { - ctx.reply("Empty ID list provided").await?; - return Ok(()); + if ids.is_empty() { + return Err(anyhow!("Empty ID list provided").tag(ErrorKind::User)); } let mut count = 0; @@ -472,7 +484,8 @@ pub async fn delete( mut ctx: Context<'_>, #[description = "Id of score to delete"] ids: Vec, ) -> Result<(), Error> { - delete_impl(&mut ctx, &ids).await?; + let res = delete_impl(&mut ctx, &ids).await; + ctx.handle_error(res).await?; Ok(()) } diff --git a/src/commands/stats.rs b/src/commands/stats.rs index a7adb85..6cdf217 100644 --- a/src/commands/stats.rs +++ b/src/commands/stats.rs @@ -18,10 +18,11 @@ use crate::assets::{ TOP_BACKGROUND, }; use crate::bitmap::{Align, BitmapCanvas, Color, LayoutDrawer, LayoutManager, Rect}; -use crate::context::{Context, Error}; +use crate::context::{Context, Error, TaggedError}; use crate::logs::debug_image_log; use crate::user::User; -use crate::{assert_is_pookie, get_user, reply_errors, timed}; + +use super::discord::MessageContext; // }}} // {{{ Stats @@ -37,31 +38,26 @@ pub async fn stats(_ctx: Context<'_>) -> Result<(), Error> { } // }}} // {{{ Render best plays -async fn best_plays( - ctx: &mut Context<'_>, +async fn best_plays( + ctx: &mut C, user: &User, scoring_system: ScoringSystem, grid_size: (u32, u32), require_full: bool, -) -> Result<(), Error> { +) -> Result<(), TaggedError> { let user_ctx = ctx.data(); - let plays = reply_errors!( - ctx, - timed!("get_best_plays", { - get_best_plays( - user_ctx, - user.id, - scoring_system, - if require_full { - grid_size.0 * grid_size.1 - } else { - grid_size.0 * (grid_size.1.max(1) - 1) + 1 - } as usize, - (grid_size.0 * grid_size.1) as usize, - None, - )? - }) - ); + let plays = get_best_plays( + user_ctx, + user.id, + scoring_system, + if require_full { + grid_size.0 * grid_size.1 + } else { + grid_size.0 * (grid_size.1.max(1) - 1) + 1 + } as usize, + (grid_size.0 * grid_size.1) as usize, + None, + )?; // {{{ Layout let mut layout = LayoutManager::default(); @@ -132,7 +128,7 @@ async fn best_plays( let bg_center = Rect::from_image(bg).center(); // Draw background - drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg); + drawer.blit_rbga(item_area, (-8, jacket_margin), bg); with_font(&EXO_FONT, |faces| { drawer.text( item_area, @@ -420,20 +416,49 @@ async fn best_plays( } // }}} // {{{ B30 +// {{{ Implementation +pub async fn b30_impl( + ctx: &mut C, + scoring_system: Option, +) -> Result<(), TaggedError> { + let user = User::from_context(ctx)?; + best_plays(ctx, &user, scoring_system.unwrap_or_default(), (5, 6), true).await?; + Ok(()) +} +// }}} +// {{{ Discord wrapper /// Show the 30 best scores #[poise::command(prefix_command, slash_command, user_cooldown = 30)] pub async fn b30(mut ctx: Context<'_>, scoring_system: Option) -> Result<(), Error> { - let user = get_user!(&mut ctx); + let res = b30_impl(&mut ctx, scoring_system).await; + ctx.handle_error(res).await?; + Ok(()) +} +// }}} +// }}} +// {{{ B-any +// {{{ Implementation +async fn bany_impl( + ctx: &mut C, + scoring_system: Option, + width: u32, + height: u32, +) -> Result<(), TaggedError> { + let user = User::from_context(ctx)?; + user.assert_is_pookie()?; best_plays( - &mut ctx, + ctx, &user, scoring_system.unwrap_or_default(), - (5, 6), - true, + (width, height), + false, ) - .await -} + .await?; + Ok(()) +} +// }}} +// {{{ Discord wrapper #[poise::command(prefix_command, slash_command, hide_in_help, global_cooldown = 5)] pub async fn bany( mut ctx: Context<'_>, @@ -441,23 +466,16 @@ pub async fn bany( width: u32, height: u32, ) -> Result<(), Error> { - let user = get_user!(&mut ctx); - assert_is_pookie!(ctx, user); - best_plays( - &mut ctx, - &user, - scoring_system.unwrap_or_default(), - (width, height), - false, - ) - .await + let res = bany_impl(&mut ctx, scoring_system, width, height).await; + ctx.handle_error(res).await?; + Ok(()) } // }}} +// }}} // {{{ Meta -/// Show stats about the bot itself. -#[poise::command(prefix_command, slash_command, user_cooldown = 1)] -async fn meta(mut ctx: Context<'_>) -> Result<(), Error> { - let user = get_user!(&mut ctx); +// {{{ Implementation +async fn meta_impl(ctx: &mut C) -> Result<(), TaggedError> { + let user = User::from_context(ctx)?; let conn = ctx.data().db.get()?; let song_count: usize = conn .prepare_cached("SELECT count() as count FROM songs")? @@ -504,8 +522,10 @@ async fn meta(mut ctx: Context<'_>) -> Result<(), Error> { .field("Plays", format!("{play_count}"), true) .field("Your plays", format!("{your_play_count}"), true); - ctx.send(CreateReply::default().embed(embed)).await?; + ctx.send(CreateReply::default().reply(true).embed(embed)) + .await?; + // TODO: remove once achivement system is implemented println!( "{:?}", GoalStats::make(ctx.data(), &user, ScoringSystem::Standard).await? @@ -514,3 +534,14 @@ async fn meta(mut ctx: Context<'_>) -> Result<(), Error> { Ok(()) } // }}} +// {{{ Discord wrapper +/// Show stats about the bot itself. +#[poise::command(prefix_command, slash_command, user_cooldown = 1)] +async fn meta(mut ctx: Context<'_>) -> Result<(), Error> { + let res = meta_impl(&mut ctx).await; + ctx.handle_error(res).await?; + + Ok(()) +} +// }}} +// }}} diff --git a/src/commands/utils/mod.rs b/src/commands/utils/mod.rs index 9b4594c..b4dacad 100644 --- a/src/commands/utils/mod.rs +++ b/src/commands/utils/mod.rs @@ -10,37 +10,3 @@ macro_rules! edit_reply { $handle.edit($ctx, edited) }}; } - -#[macro_export] -macro_rules! get_user { - ($ctx:expr) => {{ - crate::reply_errors!($ctx, crate::user::User::from_context($ctx)) - }}; -} - -#[macro_export] -macro_rules! assert_is_pookie { - ($ctx:expr, $user:expr) => {{ - if !$user.is_pookie { - $ctx.reply("This feature is reserved for my pookies. Sowwy :3") - .await?; - return Ok(()); - } - }}; -} - -#[macro_export] -macro_rules! reply_errors { - ($ctx:expr, $default:expr, $value:expr) => { - match $value { - Ok(v) => v, - Err(err) => { - crate::commands::discord::MessageContext::reply($ctx, &format!("{err}")).await?; - return Ok($default); - } - } - }; - ($ctx:expr, $value:expr) => { - crate::reply_errors!($ctx, Default::default(), $value) - }; -} diff --git a/src/context.rs b/src/context.rs index 15b1505..77656eb 100644 --- a/src/context.rs +++ b/src/context.rs @@ -9,6 +9,7 @@ use std::sync::LazyLock; use crate::arcaea::{chart::SongCache, jacket::JacketCache}; use crate::assets::{get_data_dir, EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT}; +use crate::commands::discord::MessageContext; use crate::recognition::{hyperglass::CharMeasurements, ui::UIMeasurements}; use crate::timed; // }}} @@ -17,6 +18,70 @@ use crate::timed; pub type Error = anyhow::Error; pub type Context<'a> = poise::Context<'a, UserContext, Error>; // }}} +// {{{ Error handling +#[derive(Debug, Clone, Copy)] +pub enum ErrorKind { + User, + Internal, +} + +#[derive(Debug)] +pub struct TaggedError { + pub kind: ErrorKind, + pub error: Error, +} + +impl TaggedError { + #[inline] + pub fn new(kind: ErrorKind, error: Error) -> Self { + Self { kind, error } + } +} + +#[macro_export] +macro_rules! get_user_error { + ($err:expr) => {{ + match $err.kind { + $crate::context::ErrorKind::User => $err.error, + $crate::context::ErrorKind::Internal => Err($err.error)?, + } + }}; +} + +/// Handles a [TaggedError], showing user errors to the user, +/// and throwing away anything else. +pub async fn discord_error_handler( + ctx: &mut impl MessageContext, + res: Result, +) -> Result, Error> { + match res { + Ok(v) => Ok(Some(v)), + Err(e) => match e.kind { + ErrorKind::Internal => Err(e.error), + ErrorKind::User => { + ctx.reply(&format!("{}", e.error)).await?; + Ok(None) + } + }, + } +} + +impl> From for TaggedError { + fn from(value: E) -> Self { + Self::new(ErrorKind::Internal, value.into()) + } +} + +pub trait TagError { + fn tag(self, tag: ErrorKind) -> TaggedError; +} + +impl TagError for Error { + fn tag(self, tag: ErrorKind) -> TaggedError { + TaggedError::new(tag, self) + } +} +// }}} // {{{ DB connection pub type DbConnection = r2d2::Pool; @@ -106,7 +171,7 @@ pub mod testing { .await } - pub fn import_songs_and_jackets_from(to: &Path) -> () { + pub fn import_songs_and_jackets_from(to: &Path) { let out = std::process::Command::new("scripts/copy-chart-info.sh") .arg(get_data_dir()) .arg(to) @@ -124,16 +189,17 @@ pub mod testing { ($test_path:expr, $f:expr) => {{ use std::str::FromStr; - let mut data = (*crate::context::testing::get_shared_context().await).clone(); + let mut data = (*$crate::context::testing::get_shared_context().await).clone(); let dir = tempfile::tempdir()?; - data.db = crate::context::connect_db(dir.path()); - crate::context::testing::import_songs_and_jackets_from(dir.path()); + data.db = $crate::context::connect_db(dir.path()); + $crate::context::testing::import_songs_and_jackets_from(dir.path()); - let mut ctx = crate::commands::discord::mock::MockContext::new(data); - crate::user::User::create_from_context(&ctx)?; + let mut ctx = $crate::commands::discord::mock::MockContext::new(data); + let res = $crate::user::User::create_from_context(&ctx); + ctx.handle_error(res).await?; - let res: Result<(), Error> = $f(&mut ctx).await; - res?; + let res: Result<(), $crate::context::TaggedError> = $f(&mut ctx).await; + ctx.handle_error(res).await?; ctx.golden(&std::path::PathBuf::from_str($test_path)?)?; Ok(()) diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..257ab52 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,21 @@ +#![allow(async_fn_in_trait)] +#![feature(iter_map_windows)] +#![feature(let_chains)] +#![feature(array_try_map)] +#![feature(async_closure)] +#![feature(try_blocks)] +#![feature(thread_local)] +#![feature(generic_arg_infer)] +#![feature(iter_collect_into)] + +pub mod arcaea; +pub mod assets; +pub mod bitmap; +pub mod commands; +pub mod context; +pub mod levenshtein; +pub mod logs; +pub mod recognition; +pub mod time; +pub mod transform; +pub mod user; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 86696fc..0000000 --- a/src/main.rs +++ /dev/null @@ -1,126 +0,0 @@ -#![warn(clippy::str_to_string)] -#![feature(iter_map_windows)] -#![feature(let_chains)] -#![feature(array_try_map)] -#![feature(async_closure)] -#![feature(try_blocks)] -#![feature(thread_local)] -#![feature(generic_arg_infer)] -#![feature(lazy_cell_consume)] -#![feature(iter_collect_into)] - -mod arcaea; -mod assets; -mod bitmap; -mod cli; -mod commands; -mod context; -mod levenshtein; -mod logs; -mod recognition; -mod time; -mod transform; -mod user; - -use arcaea::play::generate_missing_scores; -use clap::Parser; -use cli::{Cli, Command}; -use context::{Error, UserContext}; -use poise::serenity_prelude::{self as serenity}; -use std::{env::var, sync::Arc, time::Duration}; - -// {{{ Error handler -async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) { - match error { - error => { - if let Err(e) = poise::builtins::on_error(error).await { - println!("Error while handling error: {}", e) - } - } - } -} -// }}} - -#[tokio::main] -async fn main() { - let cli = Cli::parse(); - match cli.command { - Command::Discord {} => { - // {{{ Poise options - let options = poise::FrameworkOptions { - commands: vec![ - commands::help(), - commands::score::score(), - commands::stats::stats(), - commands::chart::chart(), - ], - prefix_options: poise::PrefixFrameworkOptions { - stripped_dynamic_prefix: Some(|_ctx, message, _user_ctx| { - Box::pin(async { - if message.author.bot || Into::::into(message.author.id) == 1 { - Ok(None) - } else if message.content.starts_with("!") { - Ok(Some(message.content.split_at(1))) - } else if message.guild_id.is_none() { - if message.content.trim().len() == 0 { - Ok(Some(("", "score magic"))) - } else { - Ok(Some(("", &message.content[..]))) - } - } else { - Ok(None) - } - }) - }), - edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan( - Duration::from_secs(3600), - ))), - ..Default::default() - }, - on_error: |error| Box::pin(on_error(error)), - ..Default::default() - }; - // }}} - // {{{ Start poise - let framework = poise::Framework::builder() - .setup(move |ctx, _ready, framework| { - Box::pin(async move { - println!("Logged in as {}", _ready.user.name); - poise::builtins::register_globally(ctx, &framework.options().commands) - .await?; - let ctx = UserContext::new().await?; - - if var("SHIMMERING_REGEN_SCORES").unwrap_or_default() == "1" { - timed!("generate_missing_scores", { - generate_missing_scores(&ctx).await?; - }); - } - - Ok(ctx) - }) - }) - .options(options) - .build(); - - let token = var("SHIMMERING_DISCORD_TOKEN") - .expect("Missing `SHIMMERING_DISCORD_TOKEN` env var"); - let intents = serenity::GatewayIntents::non_privileged() - | serenity::GatewayIntents::MESSAGE_CONTENT; - - let client = serenity::ClientBuilder::new(token, intents) - .framework(framework) - .await; - - client.unwrap().start().await.unwrap() - // }}} - } - Command::PrepareJackets {} => { - cli::prepare_jackets::run().expect("Could not prepare jackets"); - } - Command::Analyse(args) => { - cli::analyse::run(args) - .await - .expect("Could not analyse screenshot"); - } - } -} diff --git a/src/recognition/fuzzy_song_name.rs b/src/recognition/fuzzy_song_name.rs index 004b16a..5563c7e 100644 --- a/src/recognition/fuzzy_song_name.rs +++ b/src/recognition/fuzzy_song_name.rs @@ -45,7 +45,7 @@ pub fn guess_song_and_chart<'a>( .or_else(|| strip_case_insensitive_suffix(name, "[ETR]").zip(Some(Difficulty::ETR))) .or_else(|| strip_case_insensitive_suffix(name, "BYD").zip(Some(Difficulty::BYD))) .or_else(|| strip_case_insensitive_suffix(name, "[BYD]").zip(Some(Difficulty::BYD))) - .unwrap_or((&name, Difficulty::FTR)); + .unwrap_or((name, Difficulty::FTR)); guess_chart_name(name, &ctx.song_cache, Some(difficulty), true) } @@ -85,7 +85,7 @@ pub fn guess_chart_name<'a>( distance_vec.clear(); // Apply raw distance - let base_distance = edit_distance_with(&text, &song_title, &mut levenshtein_vec); + let base_distance = edit_distance_with(text, song_title, &mut levenshtein_vec); if base_distance <= song.title.len() / 3 { distance_vec.push(base_distance * 10 + 2); } @@ -95,7 +95,7 @@ pub fn guess_chart_name<'a>( if let Some(sliced) = &song_title.get(..shortest_len) && (text.len() >= 6 || unsafe_heuristics) { - let slice_distance = edit_distance_with(&text, sliced, &mut levenshtein_vec); + let slice_distance = edit_distance_with(text, sliced, &mut levenshtein_vec); if slice_distance == 0 { distance_vec.push(3); } @@ -105,7 +105,7 @@ pub fn guess_chart_name<'a>( if let Some(shorthand) = &chart.shorthand && unsafe_heuristics { - let short_distance = edit_distance_with(&text, shorthand, &mut levenshtein_vec); + let short_distance = edit_distance_with(text, shorthand, &mut levenshtein_vec); if short_distance <= shorthand.len() / 3 { distance_vec.push(short_distance * 10 + 1); } @@ -121,7 +121,7 @@ pub fn guess_chart_name<'a>( close_enough.sort_by_key(|(song, _, _)| song.id); close_enough.dedup_by_key(|(song, _, _)| song.id); - if close_enough.len() == 0 { + if close_enough.is_empty() { if text.len() <= 1 { bail!( "Could not find match for chart name '{}' [{:?}]", @@ -133,13 +133,11 @@ pub fn guess_chart_name<'a>( } } else if close_enough.len() == 1 { break (close_enough[0].0, close_enough[0].1); + } else if unsafe_heuristics { + close_enough.sort_by_key(|(_, _, distance)| *distance); + break (close_enough[0].0, close_enough[0].1); } else { - if unsafe_heuristics { - close_enough.sort_by_key(|(_, _, distance)| *distance); - break (close_enough[0].0, close_enough[0].1); - } else { - bail!("Name '{}' is too vague to choose a match", raw_text); - }; + bail!("Name '{}' is too vague to choose a match", raw_text); }; }; diff --git a/src/recognition/recognize.rs b/src/recognition/recognize.rs index 9bfa63f..a8bbcf0 100644 --- a/src/recognition/recognize.rs +++ b/src/recognition/recognize.rs @@ -6,7 +6,8 @@ use hypertesseract::{PageSegMode, Tesseract}; use image::imageops::FilterType; use image::{DynamicImage, GenericImageView}; use num::integer::Roots; -use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage}; +use poise::serenity_prelude::{CreateAttachment, CreateEmbed}; +use poise::CreateReply; use crate::arcaea::chart::{Chart, Difficulty, Song, DIFFICULTY_MENU_PIXEL_COLORS}; use crate::arcaea::jacket::IMAGE_VEC_DIM; @@ -114,13 +115,16 @@ impl ImageAnalyzer { "An error occurred, around the time I was extracting data for {ui_rect:?}" )); - let msg = CreateMessage::default().embed(embed); - ctx.send_files([error_attachement], msg).await?; + ctx.send( + CreateReply::default() + .embed(embed) + .attachment(error_attachement), + ) + .await?; } else { embed = embed.title("An error occurred"); - let msg = CreateMessage::default().embed(embed); - ctx.send_files([], msg).await?; + ctx.send(CreateReply::default().embed(embed)).await?; } Ok(()) @@ -355,9 +359,9 @@ impl ImageAnalyzer { } // }}} // {{{ Read max recall - pub fn read_max_recall<'a>( + pub fn read_max_recall( &mut self, - ctx: &'a UserContext, + ctx: &UserContext, image: &DynamicImage, ) -> Result { let image = self.interp_crop(ctx, image, ScoreScreen(ScoreScreenRect::MaxRecall))?; diff --git a/src/user.rs b/src/user.rs index 556d8dd..f2f3207 100644 --- a/src/user.rs +++ b/src/user.rs @@ -2,7 +2,7 @@ use anyhow::anyhow; use rusqlite::Row; use crate::commands::discord::MessageContext; -use crate::context::{Error, UserContext}; +use crate::context::{ErrorKind, TagError, TaggedError, UserContext}; #[derive(Debug, Clone)] pub struct User { @@ -13,7 +13,7 @@ pub struct User { impl User { #[inline] - fn from_row<'a, 'b>(row: &'a Row<'b>) -> Result { + fn from_row(row: &Row<'_>) -> Result { Ok(Self { id: row.get("id")?, discord_id: row.get("discord_id")?, @@ -21,7 +21,7 @@ impl User { }) } - pub fn create_from_context(ctx: &impl MessageContext) -> Result { + pub fn create_from_context(ctx: &impl MessageContext) -> Result { let discord_id = ctx.author_id().to_string(); let user_id: u32 = ctx .data() @@ -35,7 +35,7 @@ impl User { )? .query_map([&discord_id], |row| row.get("id"))? .next() - .ok_or_else(|| anyhow!("Failed to create user"))??; + .ok_or_else(|| anyhow!("No id returned from user creation"))??; Ok(Self { discord_id, @@ -44,7 +44,7 @@ impl User { }) } - pub fn from_context(ctx: &impl MessageContext) -> Result { + pub fn from_context(ctx: &impl MessageContext) -> Result { let id = ctx.author_id(); let user = ctx .data() @@ -53,20 +53,35 @@ impl User { .prepare_cached("SELECT * FROM users WHERE discord_id = ?")? .query_map([id], Self::from_row)? .next() - .ok_or_else(|| anyhow!("You are not an user in my database, sowwy ^~^"))??; + .ok_or_else(|| { + anyhow!("You are not an user in my database, sowwy ^~^").tag(ErrorKind::User) + })??; Ok(user) } - pub fn by_id(ctx: &UserContext, id: u32) -> Result { + pub fn by_id(ctx: &UserContext, id: u32) -> Result { let user = ctx .db .get()? .prepare_cached("SELECT * FROM users WHERE id = ?")? .query_map([id], Self::from_row)? .next() - .ok_or_else(|| anyhow!("You are not an user in my database, sowwy ^~^"))??; + .ok_or_else(|| { + anyhow!("You are not an user in my database, sowwy ^~^").tag(ErrorKind::User) + })??; Ok(user) } + + #[inline] + pub fn assert_is_pookie(&self) -> Result<(), TaggedError> { + if !self.is_pookie { + return Err( + anyhow!("This feature is reserved for my pookies. Sowwy :3").tag(ErrorKind::User) + ); + } + + Ok(()) + } }