diff --git a/youmubot-osu/src/discord/embeds.rs b/youmubot-osu/src/discord/embeds.rs index c760d78..1781997 100644 --- a/youmubot-osu/src/discord/embeds.rs +++ b/youmubot-osu/src/discord/embeds.rs @@ -272,7 +272,10 @@ impl<'a> ScoreEmbedBuilder<'a> { self } pub fn footer(&mut self, footer: impl Into) -> &mut Self { - self.footer = Some(footer.into()); + self.footer = Some(match self.footer.take() { + None => footer.into(), + Some(pre) => format!("{} | {}", pre, footer.into()), + }); self } } diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 474d7bb..9ad705d 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -148,6 +148,7 @@ pub async fn setup( save, forcesave, recent, + pins, last, check, top, @@ -431,6 +432,7 @@ async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv) Ok(()) } +#[derive(Debug, Clone)] struct ModeArg(Mode); impl FromStr for ModeArg { @@ -464,7 +466,9 @@ async fn to_user_id_query( .ok_or_else(|| Error::msg("No saved account found")) } +#[derive(Debug, Clone, Default)] enum Nth { + #[default] All, Nth(u8), } @@ -483,6 +487,39 @@ impl FromStr for Nth { } } +#[derive(Debug, Clone)] +struct ListingArgs { + pub nth: Nth, + pub style: ScoreListStyle, + pub mode: Mode, + pub user: UserID, +} + +impl ListingArgs { + pub async fn parse( + env: &OsuEnv, + msg: &Message, + args: &mut Args, + default_style: ScoreListStyle, + ) -> Result { + let nth = args.single::().unwrap_or(Nth::All); + let style = args.single::().unwrap_or(default_style); + let mode = args.single::().unwrap_or(ModeArg(Mode::Std)).0; + let user = to_user_id_query( + args.quoted().trimmed().single::().ok(), + &env, + msg.author.id, + ) + .await?; + Ok(Self { + nth, + style, + mode, + user, + }) + } +} + #[command] #[aliases("rs", "rc", "r")] #[description = "Gets an user's recent play"] @@ -493,15 +530,12 @@ impl FromStr for Nth { pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let env = ctx.data.read().await.get::().unwrap().clone(); - let nth = args.single::().unwrap_or(Nth::All); - let style = args.single::().unwrap_or_default(); - let mode = args.single::().unwrap_or(ModeArg(Mode::Std)).0; - let user = to_user_id_query( - args.quoted().trimmed().single::().ok(), - &env, - msg.author.id, - ) - .await?; + let ListingArgs { + nth, + style, + mode, + user, + } = ListingArgs::parse(&env, msg, &mut args, ScoreListStyle::Table).await?; let osu_client = &env.client; @@ -509,39 +543,11 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu .user(&user, |f| f.mode(mode)) .await? .ok_or_else(|| Error::msg("User not found"))?; + let plays = osu_client + .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50)) + .await?; match nth { - Nth::Nth(nth) => { - let recent_play = osu_client - .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(nth)) - .await? - .into_iter() - .last() - .ok_or_else(|| Error::msg("No such play"))?; - let beatmap = env - .beatmaps - .get_beatmap(recent_play.beatmap_id, mode) - .await?; - let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?; - let beatmap_mode = BeatmapWithMode(beatmap, mode); - - msg.channel_id - .send_message( - &ctx, - CreateMessage::new() - .content("Here is the play that you requested".to_string()) - .embed(score_embed(&recent_play, &beatmap_mode, &content, &user).build()) - .components(vec![score_components(msg.guild_id)]) - .reference_message(msg), - ) - .await?; - - // Save the beatmap... - cache::save_beatmap(&env, msg.channel_id, &beatmap_mode).await?; - } Nth::All => { - let plays = osu_client - .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50)) - .await?; let reply = msg .reply( ctx, @@ -552,6 +558,101 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu .display_scores(plays, mode, ctx, reply.guild_id, reply) .await?; } + Nth::Nth(nth) => { + let Some(play) = plays.get(nth as usize) else { + Err(Error::msg("No such play"))? + }; + let attempts = plays + .iter() + .skip(nth as usize) + .take_while(|p| p.beatmap_id == play.beatmap_id && p.mods == play.mods) + .count(); + let beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?; + let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?; + let beatmap_mode = BeatmapWithMode(beatmap, mode); + + msg.channel_id + .send_message( + &ctx, + CreateMessage::new() + .content("Here is the play that you requested".to_string()) + .embed( + score_embed(play, &beatmap_mode, &content, &user) + .footer(format!("Attempt #{}", attempts)) + .build(), + ) + .components(vec![score_components(msg.guild_id)]) + .reference_message(msg), + ) + .await?; + + // Save the beatmap... + cache::save_beatmap(&env, msg.channel_id, &beatmap_mode).await?; + } + } + Ok(()) +} + +#[command] +#[aliases("pin")] +#[description = "Gets an user's pinned plays"] +#[usage = "#[the nth recent play = --all] / [style (table or grid) = --table] / [mode (std, taiko, mania, catch) = std] / [username / user id = your saved id]"] +#[example = "#1 / taiko / natsukagami"] +#[delimiters("/", " ")] +#[max_args(4)] +pub async fn pins(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let env = ctx.data.read().await.get::().unwrap().clone(); + + let ListingArgs { + nth, + style, + mode, + user, + } = ListingArgs::parse(&env, msg, &mut args, ScoreListStyle::Grid).await?; + + let osu_client = &env.client; + + let user = osu_client + .user(&user, |f| f.mode(mode)) + .await? + .ok_or_else(|| Error::msg("User not found"))?; + let plays = osu_client + .user_pins(UserID::ID(user.id), |f| f.mode(mode).limit(50)) + .await?; + match nth { + Nth::All => { + let reply = msg + .reply( + ctx, + format!("Here are the pinned plays by `{}`!", user.username), + ) + .await?; + style + .display_scores(plays, mode, ctx, reply.guild_id, reply) + .await?; + } + 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_mode = BeatmapWithMode(beatmap, mode); + + msg.channel_id + .send_message( + &ctx, + CreateMessage::new() + .content("Here is the play that you requested".to_string()) + .embed(score_embed(play, &beatmap_mode, &content, &user).build()) + .components(vec![score_components(msg.guild_id)]) + .reference_message(msg), + ) + .await?; + + // Save the beatmap... + cache::save_beatmap(&env, msg.channel_id, &beatmap_mode).await?; + } } Ok(()) } @@ -759,17 +860,15 @@ pub(crate) async fn do_check( #[max_args(4)] pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let env = ctx.data.read().await.get::().unwrap().clone(); - let nth = args.single::().unwrap_or(Nth::All); - let style = args.single::().unwrap_or_default(); - let mode = args - .single::() - .map(|ModeArg(t)| t) - .unwrap_or(Mode::Std); - - let user_id = to_user_id_query(args.single::().ok(), &env, msg.author.id).await?; + let ListingArgs { + nth, + style, + mode, + user, + } = ListingArgs::parse(&env, msg, &mut args, ScoreListStyle::default()).await?; let osu_client = &env.client; let user = osu_client - .user(&user_id, |f| f.mode(mode)) + .user(&user, |f| f.mode(mode)) .await? .ok_or_else(|| Error::msg("User not found"))?; @@ -813,7 +912,10 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult .user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100)) .await?; let reply = msg - .reply(&ctx, format!("Here are the top plays by `{}`!", user_id)) + .reply( + &ctx, + format!("Here are the top plays by `{}`!", user.username), + ) .await?; style .display_scores(plays, mode, ctx, msg.guild_id, reply) diff --git a/youmubot-osu/src/lib.rs b/youmubot-osu/src/lib.rs index e540261..05ac68b 100644 --- a/youmubot-osu/src/lib.rs +++ b/youmubot-osu/src/lib.rs @@ -108,6 +108,14 @@ impl OsuClient { self.user_scores(UserScoreType::Recent, user, f).await } + pub async fn user_pins( + &self, + user: UserID, + f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder, + ) -> Result, Error> { + self.user_scores(UserScoreType::Pin, user, f).await + } + async fn user_scores( &self, u: UserScoreType, diff --git a/youmubot-osu/src/request.rs b/youmubot-osu/src/request.rs index cd72cc8..87e52d3 100644 --- a/youmubot-osu/src/request.rs +++ b/youmubot-osu/src/request.rs @@ -238,6 +238,7 @@ pub mod builders { pub(crate) enum UserScoreType { Recent, Best, + Pin, } pub struct UserScoreRequestBuilder { @@ -273,6 +274,7 @@ pub mod builders { r = match self.score_type { UserScoreType::Recent => r.recent().include_fails(true), UserScoreType::Best => r.best(), + UserScoreType::Pin => r.pinned(), }; if let Some(mode) = self.mode { r = r.mode(mode.into()); diff --git a/youmubot-prelude/src/args.rs b/youmubot-prelude/src/args.rs index f9aa5c7..c6410ce 100644 --- a/youmubot-prelude/src/args.rs +++ b/youmubot-prelude/src/args.rs @@ -253,6 +253,7 @@ mod username_arg { use serenity::model::id::UserId; use std::str::FromStr; /// An argument that can be either a tagged user, or a raw string. + #[derive(Debug, Clone)] pub enum UsernameArg { Tagged(UserId), Raw(String),