osu: Implement pins (#53)

Also format recent so attempt count is displayed
This commit is contained in:
Natsu Kagami 2024-10-12 17:07:56 +02:00 committed by GitHub
parent a8d1d11223
commit 6fbae89dfe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 167 additions and 51 deletions

View file

@ -272,7 +272,10 @@ impl<'a> ScoreEmbedBuilder<'a> {
self self
} }
pub fn footer(&mut self, footer: impl Into<String>) -> &mut Self { pub fn footer(&mut self, footer: impl Into<String>) -> &mut Self {
self.footer = Some(footer.into()); self.footer = Some(match self.footer.take() {
None => footer.into(),
Some(pre) => format!("{} | {}", pre, footer.into()),
});
self self
} }
} }

View file

@ -148,6 +148,7 @@ pub async fn setup(
save, save,
forcesave, forcesave,
recent, recent,
pins,
last, last,
check, check,
top, top,
@ -431,6 +432,7 @@ async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv)
Ok(()) Ok(())
} }
#[derive(Debug, Clone)]
struct ModeArg(Mode); struct ModeArg(Mode);
impl FromStr for ModeArg { impl FromStr for ModeArg {
@ -464,7 +466,9 @@ async fn to_user_id_query(
.ok_or_else(|| Error::msg("No saved account found")) .ok_or_else(|| Error::msg("No saved account found"))
} }
#[derive(Debug, Clone, Default)]
enum Nth { enum Nth {
#[default]
All, All,
Nth(u8), 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<ListingArgs> {
let nth = args.single::<Nth>().unwrap_or(Nth::All);
let style = args.single::<ScoreListStyle>().unwrap_or(default_style);
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
let user = to_user_id_query(
args.quoted().trimmed().single::<UsernameArg>().ok(),
&env,
msg.author.id,
)
.await?;
Ok(Self {
nth,
style,
mode,
user,
})
}
}
#[command] #[command]
#[aliases("rs", "rc", "r")] #[aliases("rs", "rc", "r")]
#[description = "Gets an user's recent play"] #[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 { pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone(); let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let nth = args.single::<Nth>().unwrap_or(Nth::All); let ListingArgs {
let style = args.single::<ScoreListStyle>().unwrap_or_default(); nth,
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0; style,
let user = to_user_id_query( mode,
args.quoted().trimmed().single::<UsernameArg>().ok(), user,
&env, } = ListingArgs::parse(&env, msg, &mut args, ScoreListStyle::Table).await?;
msg.author.id,
)
.await?;
let osu_client = &env.client; 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)) .user(&user, |f| f.mode(mode))
.await? .await?
.ok_or_else(|| Error::msg("User not found"))?; .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 { 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 => { Nth::All => {
let plays = osu_client
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))
.await?;
let reply = msg let reply = msg
.reply( .reply(
ctx, 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) .display_scores(plays, mode, ctx, reply.guild_id, reply)
.await?; .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::<OsuEnv>().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(()) Ok(())
} }
@ -759,17 +860,15 @@ pub(crate) async fn do_check(
#[max_args(4)] #[max_args(4)]
pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone(); let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let nth = args.single::<Nth>().unwrap_or(Nth::All); let ListingArgs {
let style = args.single::<ScoreListStyle>().unwrap_or_default(); nth,
let mode = args style,
.single::<ModeArg>() mode,
.map(|ModeArg(t)| t) user,
.unwrap_or(Mode::Std); } = ListingArgs::parse(&env, msg, &mut args, ScoreListStyle::default()).await?;
let user_id = to_user_id_query(args.single::<UsernameArg>().ok(), &env, msg.author.id).await?;
let osu_client = &env.client; let osu_client = &env.client;
let user = osu_client let user = osu_client
.user(&user_id, |f| f.mode(mode)) .user(&user, |f| f.mode(mode))
.await? .await?
.ok_or_else(|| Error::msg("User not found"))?; .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)) .user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
.await?; .await?;
let reply = msg 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?; .await?;
style style
.display_scores(plays, mode, ctx, msg.guild_id, reply) .display_scores(plays, mode, ctx, msg.guild_id, reply)

View file

@ -108,6 +108,14 @@ impl OsuClient {
self.user_scores(UserScoreType::Recent, user, f).await self.user_scores(UserScoreType::Recent, user, f).await
} }
pub async fn user_pins(
&self,
user: UserID,
f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder,
) -> Result<Vec<Score>, Error> {
self.user_scores(UserScoreType::Pin, user, f).await
}
async fn user_scores( async fn user_scores(
&self, &self,
u: UserScoreType, u: UserScoreType,

View file

@ -238,6 +238,7 @@ pub mod builders {
pub(crate) enum UserScoreType { pub(crate) enum UserScoreType {
Recent, Recent,
Best, Best,
Pin,
} }
pub struct UserScoreRequestBuilder { pub struct UserScoreRequestBuilder {
@ -273,6 +274,7 @@ pub mod builders {
r = match self.score_type { r = match self.score_type {
UserScoreType::Recent => r.recent().include_fails(true), UserScoreType::Recent => r.recent().include_fails(true),
UserScoreType::Best => r.best(), UserScoreType::Best => r.best(),
UserScoreType::Pin => r.pinned(),
}; };
if let Some(mode) = self.mode { if let Some(mode) = self.mode {
r = r.mode(mode.into()); r = r.mode(mode.into());

View file

@ -253,6 +253,7 @@ mod username_arg {
use serenity::model::id::UserId; use serenity::model::id::UserId;
use std::str::FromStr; use std::str::FromStr;
/// An argument that can be either a tagged user, or a raw string. /// An argument that can be either a tagged user, or a raw string.
#[derive(Debug, Clone)]
pub enum UsernameArg { pub enum UsernameArg {
Tagged(UserId), Tagged(UserId),
Raw(String), Raw(String),