use super::*; use poise::CreateReply; use serenity::all::User; /// osu!-related command group. #[poise::command( slash_command, subcommands("profile", "top", "recent", "pinned", "save", "forcesave") )] pub async fn osu(_ctx: CmdContext<'_, U>) -> Result<()> { Ok(()) } /// Returns top plays for a given player. /// /// If no osu! username is given, defaults to the currently registered user. #[poise::command(slash_command)] 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"] discord_name: Option, ) -> Result<()> { let env = ctx.data().osu_env(); let username_arg = arg_from_username_or_discord(username, discord_name); let args = ListingArgs::from_params( env, index, style.unwrap_or(ScoreListStyle::Table), mode, username_arg, ctx.author().id, ) .await?; ctx.defer().await?; let osu_client = &env.client; let mut plays = osu_client .user_best(UserID::ID(args.user.id), |f| f.mode(args.mode).limit(100)) .await?; plays.sort_unstable_by(|a, b| b.pp.partial_cmp(&a.pp).unwrap()); handle_listing(ctx, plays, args, "top").await } /// Get an user's profile. #[poise::command(slash_command)] async fn profile( ctx: CmdContext<'_, U>, #[description = "Game mode"] #[rename = "mode"] mode_override: Option, #[description = "osu! username"] username: Option, #[description = "Discord username"] discord_name: Option, ) -> Result<()> { let env = ctx.data().osu_env(); let username_arg = arg_from_username_or_discord(username, discord_name); let (mode, user) = user_header_or_default_id(username_arg, env, ctx.author().id).await?; let mode = mode_override.unwrap_or(mode); ctx.defer().await?; let user = env .client .user(&UserID::ID(user.id), |f| f.mode(mode)) .await?; match user { Some(u) => { let ex = UserExtras::from_user(env, &u, mode).await?; ctx.send( CreateReply::default() .content(format!("Here is {}'s **{}** profile!", u.mention(), mode)) .embed(user_embed(u, ex)), ) .await?; } None => { ctx.reply("🔍 user not found!").await?; } }; Ok(()) } /// Returns recent plays from a given player. /// /// If no osu! username is given, defaults to the currently registered user. #[poise::command(slash_command)] async fn recent( ctx: CmdContext<'_, U>, #[description = "Index of the score"] #[min = 1] #[max = 50] index: Option, #[description = "Score listing style"] style: Option, #[description = "Game mode"] mode: Option, #[description = "osu! username"] username: Option, #[description = "Discord username"] discord_name: Option, ) -> Result<()> { let env = ctx.data().osu_env(); let args = arg_from_username_or_discord(username, discord_name); let style = style.unwrap_or(ScoreListStyle::Table); let args = ListingArgs::from_params(env, index, style, mode, args, ctx.author().id).await?; ctx.defer().await?; let osu_client = &env.client; let plays = osu_client .user_recent(UserID::ID(args.user.id), |f| f.mode(args.mode).limit(50)) .await?; handle_listing(ctx, plays, args, "recent").await } /// Returns pinned plays from a given player. /// /// If no osu! username is given, defaults to the currently registered user. #[poise::command(slash_command)] async fn pinned( ctx: CmdContext<'_, U>, #[description = "Index of the score"] #[min = 1] #[max = 50] index: Option, #[description = "Score listing style"] style: Option, #[description = "Game mode"] mode: Option, #[description = "osu! username"] username: Option, #[description = "Discord username"] discord_name: Option, ) -> Result<()> { let env = ctx.data().osu_env(); let args = arg_from_username_or_discord(username, discord_name); let style = style.unwrap_or(ScoreListStyle::Table); let args = ListingArgs::from_params(env, index, style, mode, args, ctx.author().id).await?; ctx.defer().await?; let osu_client = &env.client; let plays = osu_client .user_pins(UserID::ID(args.user.id), |f| f.mode(args.mode).limit(50)) .await?; handle_listing(ctx, plays, args, "pinned").await } /// Save your osu! profile into Youmu's database for tracking and quick commands. #[poise::command(slash_command)] pub async fn save( ctx: CmdContext<'_, U>, #[description = "The osu! username to set to"] username: String, ) -> Result<()> { let env = ctx.data().osu_env(); ctx.defer().await?; let (u, mode, score, beatmap, info) = find_save_requirements(env, username).await?; let reply = ctx .clone() .send( CreateReply::default() .content(format!( "To set your osu username to **{}**, please make your most recent play \ be the following map: `/b/{}` in **{}** mode! \ It does **not** have to be a pass, and **NF** can be used! \ React to this message with 👌 within 5 minutes when you're done!", u.username, score.beatmap_id, mode.as_str_new_site() )) .embed(beatmap_embed(&beatmap, mode, Mods::NOMOD, &info)) .components(vec![beatmap_components(mode, ctx.guild_id())]), ) .await? .into_message() .await?; handle_save_respond( ctx.serenity_context(), &env, ctx.author().id, reply, &beatmap, u, mode, ) .await?; Ok(()) } /// Force-save an osu! profile into Youmu's database for tracking and quick commands. #[poise::command(slash_command, owners_only)] pub async fn forcesave( ctx: CmdContext<'_, U>, #[description = "The osu! username to set to"] username: String, #[description = "The discord user to assign to"] discord_name: User, ) -> Result<()> { let env = ctx.data().osu_env(); let osu_client = &env.client; ctx.defer().await?; let Some(u) = osu_client .user(&UserID::from_string(username.clone()), |f| f) .await? else { return Err(Error::msg("osu! user not found")); }; add_user(discord_name.id, &u, &env).await?; let ex = UserExtras::from_user(&env, &u, u.preferred_mode).await?; ctx.send( CreateReply::default() .content( MessageBuilder::new() .push("Youmu is now tracking user ") .push(discord_name.mention().to_string()) .push(" with osu! account ") .push_bold_safe(username) .build(), ) .embed(user_embed(u, ex)), ) .await?; Ok(()) } async fn handle_listing( ctx: CmdContext<'_, U>, plays: Vec, listing_args: ListingArgs, listing_kind: &'static str, ) -> Result<()> { let env = ctx.data().osu_env(); let ListingArgs { nth, style, mode, user, } = listing_args; 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 #{} {} play by {}!", nth + 1, listing_kind, user.mention() )) .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 {} plays by {}!", listing_kind, user.mention() )) .await? .into_message() .await?; style .display_scores(plays, mode, ctx.serenity_context(), ctx.guild_id(), reply) .await?; } } Ok(()) } fn arg_from_username_or_discord( username: Option, discord_name: Option, ) -> Option { match (username, discord_name) { (Some(v), _) => Some(UsernameArg::Raw(v)), (_, Some(u)) => Some(UsernameArg::Tagged(u.id)), (None, None) => None, } }