From 2e3c6f61beee30ac68f37f364036d70d28a97589 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Fri, 29 Apr 2022 19:31:04 -0400 Subject: [PATCH] Pull info from an attached .osu/.osz file (#18) --- Cargo.lock | 315 +++++++++++++++++++++--- flake.lock | 6 +- flake.nix | 23 +- youmubot-osu/Cargo.toml | 20 +- youmubot-osu/src/discord/embeds.rs | 153 +++++++++--- youmubot-osu/src/discord/hook.rs | 97 +++++++- youmubot-osu/src/discord/mod.rs | 2 +- youmubot-osu/src/discord/oppai_cache.rs | 87 +++++-- youmubot-osu/src/models/mod.rs | 51 ++-- youmubot/src/main.rs | 2 + 10 files changed, 631 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce4456f..2e454c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,21 @@ dependencies = [ [[package]] name = "adler" -version = "0.2.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "opaque-debug", +] [[package]] name = "ahash" @@ -121,6 +133,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" + [[package]] name = "bincode" version = "1.3.1" @@ -158,6 +176,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + [[package]] name = "build_const" version = "0.2.1" @@ -172,9 +199,9 @@ checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9" [[package]] name = "byteorder" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" @@ -188,11 +215,35 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +[[package]] +name = "bzip2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cc" version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -210,10 +261,19 @@ dependencies = [ "num-integer", "num-traits", "serde", - "time", + "time 0.1.43", "winapi", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "codeforces" version = "0.3.1" @@ -239,6 +299,12 @@ dependencies = [ "syn", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "core-foundation" version = "0.9.1" @@ -255,6 +321,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + [[package]] name = "cpuid-bool" version = "0.1.2" @@ -272,9 +347,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] @@ -301,15 +376,24 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.3" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" dependencies = [ - "autocfg", "cfg-if", "lazy_static", ] +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "dashmap" version = "4.0.2" @@ -329,6 +413,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", + "subtle", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -374,9 +469,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" dependencies = [ "cfg-if", "crc32fast", @@ -625,6 +720,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.3", +] + [[package]] name = "http" version = "0.2.3" @@ -633,7 +737,7 @@ checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" dependencies = [ "bytes 1.0.1", "fnv", - "itoa", + "itoa 0.4.7", ] [[package]] @@ -682,7 +786,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 0.4.7", "pin-project 1.0.5", "socket2", "tokio", @@ -770,6 +874,21 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.47" @@ -800,9 +919,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.85" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" [[package]] name = "libsqlite3-sys" @@ -875,12 +994,11 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" dependencies = [ "adler", - "autocfg", ] [[package]] @@ -984,6 +1102,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.5.2" @@ -1029,6 +1156,14 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "osuparse" +version = "2.0.1" +source = "git+https://github.com/eltrufas/osuparse?rev=ad8f6e5e7771e7cbaa2ec96c376558f9731139af#ad8f6e5e7771e7cbaa2ec96c376558f9731139af" +dependencies = [ + "unicase", +] + [[package]] name = "parking_lot" version = "0.11.1" @@ -1054,6 +1189,29 @@ dependencies = [ "winapi", ] +[[package]] +name = "password-hash" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" +dependencies = [ + "base64ct", + "rand_core 0.6.1", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +dependencies = [ + "digest 0.10.3", + "hmac", + "password-hash", + "sha2 0.10.2", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1451,7 +1609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" dependencies = [ "indexmap", - "itoa", + "itoa 0.4.7", "ryu", "serde", ] @@ -1463,7 +1621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" dependencies = [ "form_urlencoded", - "itoa", + "itoa 0.4.7", "ryu", "serde", ] @@ -1513,26 +1671,48 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4b312c3731e3fe78a185e6b9b911a7aa715b8e31cce117975219aab2acf285d" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if", "cpuid-bool", - "digest", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "sha2" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if", "cpuid-bool", - "digest", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "slab" version = "0.4.2" @@ -1616,7 +1796,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "itoa", + "itoa 0.4.7", "libc", "libsqlite3-sys", "log", @@ -1626,7 +1806,7 @@ dependencies = [ "percent-encoding", "rustls", "serde", - "sha2", + "sha2 0.9.3", "smallvec", "sqlformat", "sqlx-rt", @@ -1655,7 +1835,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.9.3", "sqlx-core", "sqlx-rt", "syn", @@ -1689,6 +1869,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.60" @@ -1768,6 +1954,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa 1.0.1", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + [[package]] name = "tinyvec" version = "1.1.1" @@ -1938,9 +2142,9 @@ checksum = "335fb14412163adc9ed4a3e53335afaa7a4b72bdd122e5f72f51b8f1db1a131e" [[package]] name = "typenum" -version = "1.12.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicase" @@ -2303,6 +2507,7 @@ dependencies = [ "chrono", "dashmap", "lazy_static", + "osuparse", "regex", "reqwest", "rosu-pp", @@ -2312,6 +2517,7 @@ dependencies = [ "youmubot-db", "youmubot-db-sql", "youmubot-prelude", + "zip", ] [[package]] @@ -2330,3 +2536,52 @@ dependencies = [ "youmubot-db", "youmubot-db-sql", ] + +[[package]] +name = "zip" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf225bcf73bb52cbb496e70475c7bd7a3f769df699c0020f6c7bd9a96dcf0b8d" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time 0.3.9", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.10.0+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1365becbe415f3f0fcd024e2f7b45bacfb5bdd055f0dc113571394114e7bdd" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.4+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7cd17c9af1a4d6c24beb1cc54b17e2ef7b593dc92f19e9d9acad8b182bbaee" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" +dependencies = [ + "cc", + "libc", +] diff --git a/flake.lock b/flake.lock index 3088ff1..0d5ff5d 100644 --- a/flake.lock +++ b/flake.lock @@ -35,11 +35,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1648746709, - "narHash": "sha256-MSwb5UZuplAkwhZrA8TsNEX57Dy8tJODtF82aKY+amA=", + "lastModified": 1651263162, + "narHash": "sha256-OOw4ll+7Ql8Fh4NRWWXxnuSxFGD6rrLB3SdGtZrfy4I=", "owner": "nixos", "repo": "nixpkgs", - "rev": "9cd1fa9bcef3b578a732a391436aad804e03e3ca", + "rev": "e850f1e4d0a645d2ec4cd5fcc427254fd4cec79a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 2b51fc5..e44c993 100644 --- a/flake.nix +++ b/flake.nix @@ -34,16 +34,21 @@ defaultApp = apps.youmubot; # `nix develop` - devShell = pkgs.mkShell { - nativeBuildInputs = - (with pkgs; [ rustc cargo rust-analyzer rustfmt ]) ++ - nixpkgs.lib.optionals (nixpkgs.lib.strings.hasSuffix "darwin" system) (with pkgs; [ - libiconv - darwin.apple_sdk.frameworks.Security - ]); - }; - + devShell = pkgs.mkShell + { + nativeBuildInputs = + (with pkgs; [ rustc cargo rust-analyzer rustfmt ]) + ++ nixpkgs.lib.optionals (nixpkgs.lib.strings.hasSuffix "darwin" system) (with pkgs; [ + libiconv + darwin.apple_sdk.frameworks.Security + ]) + ++ nixpkgs.lib.optionals (nixpkgs.lib.strings.hasSuffix "linux" system) (with pkgs; [ + pkg-config + openssl + ]); + }; # module nixosModule = import ./module.nix defaultPackage; }); } + diff --git a/youmubot-osu/Cargo.toml b/youmubot-osu/Cargo.toml index 5e92c14..0f061fb 100644 --- a/youmubot-osu/Cargo.toml +++ b/youmubot-osu/Cargo.toml @@ -7,16 +7,18 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serenity = "0.10" -chrono = "0.4" -reqwest = "0.11" -serde = { version = "1.0", features = ["derive"] } -bitflags = "1" -lazy_static = "1" -regex = "1" -rosu-pp = "0.4" -dashmap = "4" bincode = "1" +bitflags = "1" +chrono = "0.4" +dashmap = "4" +lazy_static = "1" +osuparse = { git = "https://github.com/eltrufas/osuparse", rev = "ad8f6e5e7771e7cbaa2ec96c376558f9731139af" } +regex = "1" +reqwest = "0.11" +rosu-pp = "0.4" +serde = { version = "1.0", features = ["derive"] } +serenity = "0.10" +zip = "0.6.2" youmubot-db = { path = "../youmubot-db" } youmubot-db-sql = { path = "../youmubot-db-sql" } diff --git a/youmubot-osu/src/discord/embeds.rs b/youmubot-osu/src/discord/embeds.rs index cc8cbde..9b6702d 100644 --- a/youmubot-osu/src/discord/embeds.rs +++ b/youmubot-osu/src/discord/embeds.rs @@ -1,9 +1,10 @@ use super::BeatmapWithMode; use crate::{ discord::oppai_cache::{Accuracy, BeatmapContent, BeatmapInfo, BeatmapInfoWithPP}, - models::{Beatmap, Mode, Mods, Rank, Score, User}, + models::{Beatmap, Difficulty, Mode, Mods, Rank, Score, User}, }; use serenity::{builder::CreateEmbed, utils::MessageBuilder}; +use std::time::Duration; use youmubot_prelude::*; /// Writes a number grouped in groups of 3. @@ -55,42 +56,54 @@ fn beatmap_description(b: &Beatmap) -> String { .build() } -pub fn beatmap_embed<'a>( - b: &'_ Beatmap, +pub fn beatmap_offline_embed( + b: &'_ crate::discord::oppai_cache::BeatmapContent, m: Mode, mods: Mods, - info: Option, - c: &'a mut CreateEmbed, -) -> &'a mut CreateEmbed { - let mod_str = if mods == Mods::NOMOD { - "".to_owned() +) -> Result &mut CreateEmbed + Send + Sync>> { + let bm = b.content.clone(); + let metadata = b.metadata.clone(); + let (info, pp) = b.get_possible_pp_with(mods)?; + + let total_length = if bm.hit_objects.len() >= 1 { + Duration::from_millis( + (bm.hit_objects.last().unwrap().end_time() - bm.hit_objects.first().unwrap().start_time) + as u64, + ) } else { - format!(" {}", mods) + Duration::from_secs(0) }; - let diff = b - .difficulty - .apply_mods(mods, info.map(|(v, _)| v.stars as f64)); - c.title( - MessageBuilder::new() - .push_bold_safe(&b.artist) - .push(" - ") - .push_bold_safe(&b.title) - .push(" [") - .push_bold_safe(&b.difficulty_name) - .push("]") - .push(&mod_str) - .build(), - ) - .author(|a| { - a.name(&b.creator) - .url(format!("https://osu.ppy.sh/users/{}", b.creator_id)) - .icon_url(format!("https://a.ppy.sh/{}", b.creator_id)) - }) - .url(b.link()) - .image(b.cover_url()) - .color(0xffb6c1) - .fields(info.map(|(_, pp)| { - ( + + let diff = Difficulty { + stars: info.stars, + aim: None, // TODO: this is currently unused + speed: None, // TODO: this is currently unused + cs: bm.cs as f64, + od: bm.od as f64, + ar: bm.ar as f64, + hp: bm.hp as f64, + count_normal: bm.n_circles as u64, + count_slider: bm.n_sliders as u64, + count_spinner: bm.n_spinners as u64, + max_combo: Some(info.max_combo as u64), + bpm: bm.bpm(), + drain_length: total_length, // It's hard to calculate so maybe just skip... + total_length, + } + .apply_mods(mods, Some(info.stars)); + Ok(Box::new(move |c: &mut CreateEmbed| { + c.title(beatmap_title( + &metadata.artist, + &metadata.title, + &metadata.version, + mods, + )) + .author(|a| { + a.name(&metadata.creator) + .url(format!("https://osu.ppy.sh/users/{}", metadata.creator)) + }) + .color(0xffb6c1) + .field( "Calculated pp", format!( "95%: **{:.2}**pp, 98%: **{:.2}**pp, 99%: **{:.2}**pp, 100%: **{:.2}**pp", @@ -98,15 +111,73 @@ pub fn beatmap_embed<'a>( ), false, ) + .field("Information", diff.format_info(m, mods, None), false) + // .description(beatmap_description(b)) })) - .field("Information", diff.format_info(m, mods, b), false) - .description(beatmap_description(b)) - .footer(|f| { - if info.is_none() && mods != Mods::NOMOD { - f.text("Star difficulty not reflecting mods applied."); - } - f - }) +} + +// Some helper functions here + +/// Create a properly formatted beatmap title, in the `Artist - Title [Difficulty] +mods` format. +fn beatmap_title( + artist: impl AsRef, + title: impl AsRef, + difficulty: impl AsRef, + mods: Mods, +) -> String { + let mod_str = if mods == Mods::NOMOD { + "".to_owned() + } else { + format!(" {}", mods) + }; + MessageBuilder::new() + .push_bold_safe(artist.as_ref()) + .push(" - ") + .push_bold_safe(title.as_ref()) + .push(" [") + .push_bold_safe(difficulty.as_ref()) + .push("]") + .push(&mod_str) + .build() +} + +pub fn beatmap_embed<'a>( + b: &'_ Beatmap, + m: Mode, + mods: Mods, + info: Option, + c: &'a mut CreateEmbed, +) -> &'a mut CreateEmbed { + let diff = b + .difficulty + .apply_mods(mods, info.map(|(v, _)| v.stars as f64)); + c.title(beatmap_title(&b.artist, &b.title, &b.difficulty_name, mods)) + .author(|a| { + a.name(&b.creator) + .url(format!("https://osu.ppy.sh/users/{}", b.creator_id)) + .icon_url(format!("https://a.ppy.sh/{}", b.creator_id)) + }) + .url(b.link()) + .image(b.cover_url()) + .color(0xffb6c1) + .fields(info.map(|(_, pp)| { + ( + "Calculated pp", + format!( + "95%: **{:.2}**pp, 98%: **{:.2}**pp, 99%: **{:.2}**pp, 100%: **{:.2}**pp", + pp[0], pp[1], pp[2], pp[3] + ), + false, + ) + })) + .field("Information", diff.format_info(m, mods, b), false) + .description(beatmap_description(b)) + .footer(|f| { + if info.is_none() && mods != Mods::NOMOD { + f.text("Star difficulty not reflecting mods applied."); + } + f + }) } const MAX_DIFFS: usize = 25 - 4; diff --git a/youmubot-osu/src/discord/hook.rs b/youmubot-osu/src/discord/hook.rs index d511ffb..e1c1009 100644 --- a/youmubot-osu/src/discord/hook.rs +++ b/youmubot-osu/src/discord/hook.rs @@ -5,7 +5,7 @@ use crate::{ }; use lazy_static::lazy_static; use regex::Regex; -use serenity::{model::channel::Message, utils::MessageBuilder}; +use serenity::{builder::CreateEmbed, model::channel::Message, utils::MessageBuilder}; use std::str::FromStr; use youmubot_prelude::*; @@ -23,6 +23,101 @@ lazy_static! { ).unwrap(); } +pub fn dot_osu_hook<'a>( + ctx: &'a Context, + msg: &'a Message, +) -> std::pin::Pin> + Send + 'a>> { + Box::pin(async move { + if msg.author.bot { + return Ok(()); + } + + // Take all the .osu attachments + let mut osu_embeds = msg + .attachments + .iter() + .filter( + |a| a.filename.ends_with(".osu") && a.size < 1024 * 1024, /* 1mb */ + ) + .map(|attachment| { + let url = attachment.url.clone(); + + async move { + let data = ctx.data.read().await; + let oppai = data.get::().unwrap(); + let (beatmap, _) = oppai.download_beatmap_from_url(&url, None).await.ok()?; + let embed_fn = crate::discord::embeds::beatmap_offline_embed( + &beatmap, + Mode::from(beatmap.content.mode as u8), /*For now*/ + msg.content.trim().parse().unwrap_or(Mods::NOMOD), + ) + .ok()?; + + let mut create_embed = CreateEmbed::default(); + embed_fn(&mut create_embed); + + Some(create_embed) + } + }) + .collect::>() + .filter_map(|v| future::ready(v)) + .collect::>() + .await; + + let osz_embeds = msg + .attachments + .iter() + .filter( + |a| a.filename.ends_with(".osz") && a.size < 20 * 1024 * 1024, /* 20mb */ + ) + .map(|attachment| { + let url = attachment.url.clone(); + async move { + let data = ctx.data.read().await; + let oppai = data.get::().unwrap(); + let beatmaps = oppai.download_osz_from_url(&url).await.pls_ok()?; + Some( + beatmaps + .into_iter() + .filter_map(|beatmap| { + let embed_fn = crate::discord::embeds::beatmap_offline_embed( + &beatmap, + Mode::from(beatmap.content.mode as u8), /*For now*/ + msg.content.trim().parse().unwrap_or(Mods::NOMOD), + ) + .pls_ok()?; + + let mut create_embed = CreateEmbed::default(); + embed_fn(&mut create_embed); + Some(create_embed) + }) + .collect::>(), + ) + } + }) + .collect::>() + .filter_map(|v| future::ready(v)) + .filter(|v| future::ready(v.len() > 0)) + .collect::>() + .await + .concat(); + osu_embeds.extend(osz_embeds); + + if osu_embeds.len() > 0 { + msg.channel_id + .send_message(ctx, |f| { + f.reference_message(msg) + .content(format!("{} attached beatmaps found", osu_embeds.len())) + .add_embeds(osu_embeds) + }) + .await + .ok(); + } + + Ok(()) + }) +} + pub fn hook<'a>( ctx: &'a Context, msg: &'a Message, diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 1372f3e..0fb9a29 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -31,7 +31,7 @@ mod server_rank; use db::OsuUser; use db::{OsuLastBeatmap, OsuSavedUsers, OsuUserBests}; use embeds::{beatmap_embed, score_embed, user_embed}; -pub use hook::hook; +pub use hook::{dot_osu_hook, hook}; use server_rank::{SERVER_RANK_COMMAND, UPDATE_LEADERBOARD_COMMAND}; /// The osu! client. diff --git a/youmubot-osu/src/discord/oppai_cache.rs b/youmubot-osu/src/discord/oppai_cache.rs index a71fa45..918f2f2 100644 --- a/youmubot-osu/src/discord/oppai_cache.rs +++ b/youmubot-osu/src/discord/oppai_cache.rs @@ -1,5 +1,7 @@ use crate::mods::Mods; +use osuparse::MetadataSection; use rosu_pp::{Beatmap, BeatmapExt}; +use std::io::Read; use std::sync::Arc; use youmubot_db_sql::{models::osu as models, Pool}; use youmubot_prelude::*; @@ -7,8 +9,9 @@ use youmubot_prelude::*; /// the information collected from a download/Oppai request. #[derive(Debug)] pub struct BeatmapContent { - id: u64, - content: Arc, + id: Option, + pub metadata: MetadataSection, + pub content: Arc, } /// the output of "one" oppai run. @@ -128,25 +131,82 @@ impl BeatmapCache { BeatmapCache { client, pool } } - async fn download_beatmap(&self, id: u64) -> Result { - let content = self + fn parse_beatmap(content: impl AsRef, id: Option) -> Result { + let content = content.as_ref(); + let metadata = osuparse::parse_beatmap(content) + .map_err(|e| Error::msg(format!("Cannot parse metadata: {:?}", e)))? + .metadata; + Ok(BeatmapContent { + id, + metadata, + content: Arc::new(Beatmap::parse(content.as_bytes())?), + }) + } + + /// Downloads the given osz and try to parse every osu file in there (limited to <1mb files) + pub async fn download_osz_from_url( + &self, + url: impl reqwest::IntoUrl, + ) -> Result> { + let osz = self .client .borrow() .await? - .get(&format!("https://osu.ppy.sh/osu/{}", id)) + .get(url) .send() .await? .bytes() .await?; - let bm = BeatmapContent { - id, - content: Arc::new(Beatmap::parse(content.as_ref())?), - }; + + let mut osz = zip::read::ZipArchive::new(std::io::Cursor::new(osz.as_ref()))?; + let osu_files = osz.file_names().map(|v| v.to_owned()).collect::>(); + let osu_files = osu_files + .into_iter() + .filter(|n| n.ends_with(".osu")) + .filter_map(|v| { + let mut v = osz.by_name(&v[..]).ok()?; + if v.size() > 1024 * 1024 + /*1mb*/ + { + return None; + }; + let mut content = String::new(); + v.read_to_string(&mut content).pls_ok()?; + Self::parse_beatmap(content, None).pls_ok() + }) + .collect::>(); + Ok(osu_files) + } + + /// Downloads the beatmap from an URL and returns it. + /// Does not deal with any caching. + pub async fn download_beatmap_from_url( + &self, + url: impl reqwest::IntoUrl, + id: Option, + ) -> Result<(BeatmapContent, String)> { + let content = self + .client + .borrow() + .await? + .get(url) + .send() + .await? + .text() + .await?; + let bm = Self::parse_beatmap(&content, id)?; + Ok((bm, content)) + } + + async fn download_beatmap(&self, id: u64) -> Result { + let (bm, content) = self + .download_beatmap_from_url(&format!("https://osu.ppy.sh/osu/{}", id), Some(id)) + .await?; let mut bc = models::CachedBeatmapContent { beatmap_id: id as i64, cached_at: chrono::Utc::now(), - content: content.as_ref().to_owned(), + content: content.into_bytes(), }; bc.store(&self.pool).await?; Ok(bm) @@ -155,12 +215,7 @@ impl BeatmapCache { async fn get_beatmap_db(&self, id: u64) -> Result> { Ok(models::CachedBeatmapContent::by_id(id as i64, &self.pool) .await? - .map(|v| { - Ok(BeatmapContent { - id, - content: Arc::new(Beatmap::parse(&v.content[..])?), - }) as Result<_> - }) + .map(|v| Self::parse_beatmap(String::from_utf8(v.content)?, Some(id))) .transpose()?) } diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index dfb93c9..561cb3a 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -130,34 +130,55 @@ impl Difficulty { } /// Format the difficulty info into a short summary. - pub fn format_info(&self, mode: Mode, mods: Mods, original_beatmap: &Beatmap) -> String { - let is_not_ranked = !matches!(original_beatmap.approval, ApprovalStatus::Ranked(_)); - let three_lines = is_not_ranked; + pub fn format_info<'a>( + &self, + mode: Mode, + mods: Mods, + original_beatmap: impl Into> + 'a, + ) -> String { + let original_beatmap = original_beatmap.into(); + let is_not_ranked = !matches!( + original_beatmap.map(|v| v.approval), + Some(ApprovalStatus::Ranked(_)) + ); + let three_lines = original_beatmap.is_some() && is_not_ranked; let bpm = (self.bpm * 100.0).round() / 100.0; MessageBuilder::new() - .push(format!( - "[[Link]]({}) [[DL]]({}) [[Alt]]({}) (`{}`)", - original_beatmap.link(), - original_beatmap.download_link(false), - original_beatmap.download_link(true), - original_beatmap.short_link(Some(mode), Some(mods)) - )) + .push( + original_beatmap + .map(|original_beatmap| { + format!( + "[[Link]]({}) [[DL]]({}) [[Alt]]({}) (`{}`)", + original_beatmap.link(), + original_beatmap.download_link(false), + original_beatmap.download_link(true), + original_beatmap.short_link(Some(mode), Some(mods)) + ) + }) + .unwrap_or("**Uploaded**".to_owned()), + ) .push(if three_lines { "\n" } else { ", " }) .push_bold(format!("{:.2}⭐", self.stars)) .push(", ") .push( - original_beatmap - .difficulty - .max_combo + self.max_combo .map(|c| format!("max **{}x**, ", c)) .unwrap_or_else(|| "".to_owned()), ) .push(if is_not_ranked { - format!("status **{}**, mode ", original_beatmap.approval) + format!( + "status **{}**, mode ", + original_beatmap + .map(|v| v.approval) + .unwrap_or(ApprovalStatus::WIP) + ) } else { "".to_owned() }) - .push_bold_line(format_mode(mode, original_beatmap.mode)) + .push_bold_line(format_mode( + mode, + original_beatmap.map(|v| v.mode).unwrap_or(mode), + )) .push("CS") .push_bold(format!("{:.1}", self.cs)) .push(", AR") diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index d204cd8..b42eed6 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -82,6 +82,8 @@ async fn main() { // Set up hooks #[cfg(feature = "osu")] handler.push_hook(youmubot_osu::discord::hook); + #[cfg(feature = "osu")] + handler.push_hook(youmubot_osu::discord::dot_osu_hook); #[cfg(feature = "codeforces")] handler.push_hook(youmubot_cf::InfoHook);