From 8c4d8715f7bd81c1cf06c14ae8b68929370b0de6 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Tue, 31 Dec 2024 02:02:03 +0100 Subject: [PATCH] Implement top --- youmubot-osu/src/discord/commands.rs | 93 +++++++++++++++++++++++++++- youmubot-osu/src/discord/display.rs | 13 +++- youmubot-osu/src/discord/mod.rs | 48 ++++++++++++-- youmubot-osu/src/models/mod.rs | 9 ++- 4 files changed, 152 insertions(+), 11 deletions(-) diff --git a/youmubot-osu/src/discord/commands.rs b/youmubot-osu/src/discord/commands.rs index ce46b88..df01538 100644 --- a/youmubot-osu/src/discord/commands.rs +++ b/youmubot-osu/src/discord/commands.rs @@ -1,4 +1,6 @@ use super::*; +use poise::CreateReply; +use serenity::all::User; use youmubot_prelude::*; /// osu!-related command group. @@ -11,6 +13,93 @@ pub async fn osu(_ctx: CmdContext<'_, U>) -> Result<()> { /// /// If no osu! username is given, defaults to the currently registered user. #[poise::command(slash_command)] -async fn top(ctx: CmdContext<'_, U>, username: Option) -> Result<()> { - todo!() +async fn top( + ctx: CmdContext<'_, U>, + #[description = "Index of the score"] + #[min = 1] + #[max = 100] + index: Option, + #[description = "Score listing style"] style: Option, + #[description = "Game mode"] mode: Option, + #[description = "osu! username"] username: Option, + #[description = "Discord username"] user: Option, +) -> Result<()> { + let env = ctx.data().osu_env(); + let username_arg = match (username, user) { + (Some(v), _) => Some(UsernameArg::Raw(v)), + (_, Some(u)) => Some(UsernameArg::Tagged(u.id)), + (None, None) => None, + }; + let ListingArgs { + nth, + style, + mode, + user, + } = ListingArgs::from_params( + env, + index, + style.unwrap_or(ScoreListStyle::Table), + mode, + username_arg, + ctx.author().id, + ) + .await?; + let osu_client = &env.client; + + ctx.defer().await?; + + let mut plays = osu_client + .user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100)) + .await?; + + plays.sort_unstable_by(|a, b| b.pp.partial_cmp(&a.pp).unwrap()); + let plays = plays; + + match nth { + Nth::Nth(nth) => { + let Some(play) = plays.get(nth as usize) else { + Err(Error::msg("no such play"))? + }; + + let beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?; + let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?; + let beatmap = BeatmapWithMode(beatmap, mode); + + ctx.send({ + CreateReply::default() + .content(format!( + "Here is the #{} top play by [`{}`](<{}>)", + nth + 1, + user.username, + user.link() + )) + .embed( + score_embed(&play, &beatmap, &content, user) + .top_record(nth + 1) + .build(), + ) + .components(vec![score_components(ctx.guild_id())]) + }) + .await?; + + // Save the beatmap... + cache::save_beatmap(&env, ctx.channel_id(), &beatmap).await?; + } + Nth::All => { + let reply = ctx + .clone() + .reply(format!( + "Here are the top plays by [`{}`](<{}>)!", + user.username, + user.link() + )) + .await? + .into_message() + .await?; + style + .display_scores(plays, mode, ctx.serenity_context(), ctx.guild_id(), reply) + .await?; + } + } + Ok(()) } diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index 3c8c225..af1fbb2 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -2,16 +2,19 @@ pub use beatmapset::display_beatmapset; pub use scores::ScoreListStyle; mod scores { + use poise::ChoiceParameter; use serenity::{all::GuildId, model::channel::Message}; use youmubot_prelude::*; use crate::models::{Mode, Score}; - #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, ChoiceParameter)] /// The style for the scores list to be displayed. pub enum ScoreListStyle { + #[name = "ASCII Table"] Table, + #[name = "List of Embeds"] Grid, } @@ -166,7 +169,11 @@ mod scores { } paginate_with_first_message( - Paginate { scores, mode }, + Paginate { + header: on.content.clone(), + scores, + mode, + }, ctx, on, std::time::Duration::from_secs(60), @@ -176,6 +183,7 @@ mod scores { } pub struct Paginate { + header: String, scores: Vec, mode: Mode, } @@ -311,6 +319,7 @@ mod scores { let score_table = table_formatting(&SCORE_HEADERS, &SCORE_ALIGNS, score_arr); let content = serenity::utils::MessageBuilder::new() + .push_line(&self.header) .push_line(score_table) .push_line(format!("Page **{}/{}**", page + 1, self.total_pages())) .push_line("[?] means pp was predicted by oppai-rs.") diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index d0a1884..6cae30c 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -569,6 +569,28 @@ struct ListingArgs { } impl ListingArgs { + pub async fn from_params( + env: &OsuEnv, + index: Option, + style: ScoreListStyle, + mode_override: Option, + user: Option, + sender: serenity::all::UserId, + ) -> Result { + let nth = index + .filter(|&v| 1 <= v && v <= 100) + .map(|v| v - 1) + .map(Nth::Nth) + .unwrap_or_default(); + let (mode, user) = user_header_or_default_id(user, env, sender).await?; + let mode = mode_override.unwrap_or(mode); + Ok(Self { + nth, + style, + mode, + user, + }) + } pub async fn parse( env: &OsuEnv, msg: &Message, @@ -590,10 +612,10 @@ impl ListingArgs { } } -async fn user_header_from_args( +async fn user_header_or_default_id( arg: Option, env: &OsuEnv, - msg: &Message, + default_user: serenity::all::UserId, ) -> Result<(Mode, UserHeader)> { let (mode, user) = match arg { Some(UsernameArg::Raw(r)) => { @@ -611,7 +633,7 @@ async fn user_header_from_args( (user.preferred_mode, user.into()) } None => { - let user = env.saved_users.by_user_id(msg.author.id).await? + let user = env.saved_users.by_user_id(default_user).await? .ok_or(Error::msg("You do not have a saved account! Use `osu save` command to save your osu! account."))?; (user.preferred_mode, user.into()) } @@ -619,6 +641,14 @@ async fn user_header_from_args( Ok((mode, user)) } +async fn user_header_from_args( + arg: Option, + env: &OsuEnv, + msg: &Message, +) -> Result<(Mode, UserHeader)> { + user_header_or_default_id(arg, env, msg.author.id).await +} + #[command] #[aliases("rs", "rc", "r")] #[description = "Gets an user's recent play"] @@ -977,8 +1007,10 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult .send_message(&ctx, { CreateMessage::new() .content(format!( - "{}: here is the play that you requested", - msg.author + "Here is the #{} top play by [`{}`](<{}>)", + nth + 1, + user.username, + user.link() )) .embed( score_embed(&play, &beatmap, &content, user) @@ -996,7 +1028,11 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult let reply = msg .reply( &ctx, - format!("Here are the top plays by `{}`!", user.username), + format!( + "Here are the top plays by [`{}`](<{}>)!", + user.username, + user.link() + ), ) .await?; style diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index aa6ab5c..3e9a4d9 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -10,6 +10,7 @@ pub mod mods; pub(crate) mod rosu; pub use mods::Mods; +use poise::ChoiceParameter; use serenity::utils::MessageBuilder; #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -266,11 +267,17 @@ impl fmt::Display for Language { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, std::hash::Hash)] +#[derive( + Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, std::hash::Hash, ChoiceParameter, +)] pub enum Mode { + #[name = "osu!"] Std, + #[name = "osu!taiko"] Taiko, + #[name = "osu!catch"] Catch, + #[name = "osu!mania"] Mania, }