diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index 7dfb8da..4f6d882 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -1,7 +1,126 @@ pub use beatmapset::display_beatmapset; -pub use scores::table::display_scores_table; +pub use scores::ScoreListStyle; mod scores { + use crate::models::{Mode, Score}; + use serenity::{framework::standard::CommandResult, model::channel::Message}; + use youmubot_prelude::*; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + /// The style for the scores list to be displayed. + pub enum ScoreListStyle { + Table, + Grid, + } + + impl Default for ScoreListStyle { + fn default() -> Self { + Self::Table + } + } + + impl std::str::FromStr for ScoreListStyle { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "--table" => Ok(Self::Table), + "--grid" => Ok(Self::Grid), + _ => Err(Error::msg("unknown value")), + } + } + } + + impl ScoreListStyle { + pub async fn display_scores<'a>( + self, + scores: Vec, + mode: Mode, + ctx: &'a Context, + m: &'a Message, + ) -> CommandResult { + match self { + ScoreListStyle::Table => table::display_scores_table(scores, mode, ctx, m).await, + ScoreListStyle::Grid => grid::display_scores_grid(scores, mode, ctx, m).await, + } + } + } + + pub mod grid { + use crate::discord::{ + cache::save_beatmap, BeatmapCache, BeatmapMetaCache, BeatmapWithMode, + }; + use crate::models::{Mode, Score}; + use serenity::{framework::standard::CommandResult, model::channel::Message}; + use youmubot_prelude::*; + + pub async fn display_scores_grid<'a>( + scores: Vec, + mode: Mode, + ctx: &'a Context, + m: &'a Message, + ) -> CommandResult { + if scores.is_empty() { + m.reply(&ctx, "No plays found").await?; + return Ok(()); + } + + paginate_reply( + Paginate { scores, mode }, + ctx, + m, + std::time::Duration::from_secs(60), + ) + .await?; + Ok(()) + } + + pub struct Paginate { + scores: Vec, + mode: Mode, + } + + #[async_trait] + impl pagination::Paginate for Paginate { + async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result { + let data = ctx.data.read().await; + let client = data.get::().unwrap(); + let osu = data.get::().unwrap(); + let beatmap_cache = data.get::().unwrap(); + let page = page as usize; + let score = &self.scores[page]; + + let hourglass = msg.react(ctx, '⌛').await?; + let mode = self.mode; + let beatmap = osu.get_beatmap(score.beatmap_id, mode).await?; + let content = beatmap_cache.get_beatmap(beatmap.beatmap_id).await?; + let bm = BeatmapWithMode(beatmap, mode); + let user = client + .user(crate::request::UserID::ID(score.user_id), |f| f) + .await? + .ok_or_else(|| Error::msg("user not found"))?; + + msg.edit(ctx, |e| { + e.embed(|e| { + crate::discord::embeds::score_embed(score, &bm, &content, &user) + .footer(format!("Page {}/{}", page + 1, self.scores.len())) + .build(e) + }) + }) + .await?; + save_beatmap(&*ctx.data.read().await, msg.channel_id, &bm).await?; + + // End + hourglass.delete(ctx).await?; + Ok(true) + } + + fn len(&self) -> Option { + Some(self.scores.len()) + } + } + } + pub mod table { use crate::discord::{Beatmap, BeatmapCache, BeatmapInfo, BeatmapMetaCache}; use crate::models::{Mode, Score}; diff --git a/youmubot-osu/src/discord/embeds.rs b/youmubot-osu/src/discord/embeds.rs index 3fbada3..1b6467c 100644 --- a/youmubot-osu/src/discord/embeds.rs +++ b/youmubot-osu/src/discord/embeds.rs @@ -169,6 +169,7 @@ pub(crate) struct ScoreEmbedBuilder<'a> { u: &'a User, top_record: Option, world_record: Option, + footer: Option, } impl<'a> ScoreEmbedBuilder<'a> { @@ -180,6 +181,10 @@ impl<'a> ScoreEmbedBuilder<'a> { self.world_record = Some(rank); self } + pub fn footer(&mut self, footer: impl Into) -> &mut Self { + self.footer = Some(footer.into()); + self + } } pub(crate) fn score_embed<'a>( @@ -195,12 +200,13 @@ pub(crate) fn score_embed<'a>( u, top_record: None, world_record: None, + footer: None, } } impl<'a> ScoreEmbedBuilder<'a> { #[allow(clippy::many_single_char_names)] - pub fn build<'b>(&self, m: &'b mut CreateEmbed) -> &'b mut CreateEmbed { + pub fn build<'b>(&mut self, m: &'b mut CreateEmbed) -> &'b mut CreateEmbed { let mode = self.bm.mode(); let b = &self.bm.0; let s = self.s; @@ -358,8 +364,12 @@ impl<'a> ScoreEmbedBuilder<'a> { ) .field("Map stats", diff.format_info(mode, s.mods, b), false) .timestamp(&s.date); + let mut footer = self.footer.take().unwrap_or_else(String::new); if mode.to_oppai_mode().is_none() && s.mods != Mods::NOMOD { - m.footer(|f| f.text("Star difficulty does not reflect game mods.")); + footer += " Star difficulty does not reflect game mods."; + } + if !footer.is_empty() { + m.footer(|f| f.text(footer)); } m } diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 965bf42..49a327c 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -1,6 +1,6 @@ use crate::{ discord::beatmap_cache::BeatmapMetaCache, - discord::display::display_scores_table as list_scores, + discord::display::ScoreListStyle, discord::oppai_cache::{BeatmapCache, BeatmapInfo}, models::{Beatmap, Mode, Mods, User}, request::UserID, @@ -32,7 +32,7 @@ use db::OsuUser; use db::{OsuLastBeatmap, OsuSavedUsers, OsuUserBests}; use embeds::{beatmap_embed, score_embed, user_embed}; pub use hook::hook; -use server_rank::{LEADERBOARD_COMMAND, SERVER_RANK_COMMAND, UPDATE_LEADERBOARD_COMMAND}; +use server_rank::{SERVER_RANK_COMMAND, UPDATE_LEADERBOARD_COMMAND}; /// The osu! client. pub(crate) struct OsuClient; @@ -108,7 +108,6 @@ pub fn setup( check, top, server_rank, - leaderboard, update_leaderboard )] #[default_command(std)] @@ -315,13 +314,15 @@ impl FromStr for Nth { } #[command] +#[aliases("rs", "rc")] #[description = "Gets an user's recent play"] -#[usage = "#[the nth recent play = --all] / [mode (std, taiko, mania, catch) = std] / [username / user id = your saved id]"] +#[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"] -#[max_args(3)] +#[max_args(4)] pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let data = ctx.data.read().await; 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.single::().ok(), &*data, msg).await?; @@ -361,7 +362,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu let plays = osu .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50)) .await?; - list_scores(plays, mode, ctx, msg).await?; + style.display_scores(plays, mode, ctx, msg).await?; } } Ok(()) @@ -433,9 +434,9 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult #[command] #[aliases("c", "chk")] -#[usage = "[username or tag = yourself]"] +#[usage = "[style (table or grid) = --table] / [username or tag = yourself]"] #[description = "Check your own or someone else's best record on the last beatmap. Also stores the result if possible."] -#[max_args(1)] +#[max_args(2)] pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let data = ctx.data.read().await; let bm = cache::get_beatmap(&*data, msg.channel_id).await?; @@ -448,6 +449,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul Some(bm) => { let b = &bm.0; let m = bm.1; + let style = args.single::().unwrap_or_default(); let username_arg = args.single::().ok(); let user_id = match username_arg.as_ref() { Some(UsernameArg::Tagged(v)) => Some(*v), @@ -457,9 +459,6 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul let user = to_user_id_query(username_arg, &*data, msg).await?; let osu = data.get::().unwrap(); - let oppai = data.get::().unwrap(); - - let content = oppai.get_beatmap(b.beatmap_id).await?; let user = osu .user(user, |f| f) @@ -473,22 +472,16 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul msg.reply(&ctx, "No scores found").await?; } - for score in scores.iter() { - msg.channel_id - .send_message(&ctx, |c| { - c.embed(|m| score_embed(&score, &bm, &content, &user).build(m)) - }) - .await?; - } - if let Some(user_id) = user_id { // Save to database data.get::() .unwrap() - .save(user_id, m, scores) + .save(user_id, m, scores.clone()) .await .pls_ok(); } + + style.display_scores(scores, m, ctx, msg).await?; } } @@ -497,12 +490,13 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul #[command] #[description = "Get the n-th top record of an user."] -#[usage = "[mode (std, taiko, catch, mania)] = std / #[n-th = --all] / [username or user_id = your saved user id]"] -#[example = "taiko / #2 / natsukagami"] -#[max_args(3)] +#[usage = "#[n-th = --all] / [style (table or grid) = --table] / [mode (std, taiko, catch, mania)] = std / [username or user_id = your saved user id]"] +#[example = "#2 / taiko / natsukagami"] +#[max_args(4)] pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let data = ctx.data.read().await; let nth = args.single::().unwrap_or(Nth::All); + let style = args.single::().unwrap_or_default(); let mode = args .single::() .map(|ModeArg(t)| t) @@ -555,7 +549,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult let plays = osu .user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100)) .await?; - list_scores(plays, mode, ctx, msg).await?; + style.display_scores(plays, mode, ctx, msg).await?; } } Ok(()) diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index 8c7c627..3728f44 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -5,6 +5,7 @@ use super::{ }; use crate::{ discord::{ + display::ScoreListStyle, oppai_cache::{BeatmapCache, OppaiAccuracy}, BeatmapWithMode, }, @@ -151,23 +152,33 @@ enum OrderBy { Score, } -impl From<&str> for OrderBy { - fn from(s: &str) -> Self { - if s == "--score" { - Self::Score - } else { - Self::PP +impl Default for OrderBy { + fn default() -> Self { + Self::Score + } +} + +impl std::str::FromStr for OrderBy { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "--score" => Ok(OrderBy::Score), + "--pp" => Ok(OrderBy::PP), + _ => Err(Error::msg("unknown value")), } } } -#[command("updatelb")] -#[description = "Update the leaderboard on the last seen beatmap"] -#[usage = "[--score to sort by score, default to sort by pp]"] -#[max_args(1)] +#[command("leaderboard")] +#[aliases("lb", "bmranks", "br", "cc", "updatelb")] +#[usage = "[--score to sort by score, default to sort by pp] / [--table to show a table, --grid to show score by score]"] +#[description = "See the server's ranks on the last seen beatmap"] +#[max_args(2)] #[only_in(guilds)] -pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> CommandResult { - let sort_order = OrderBy::from(args.rest()); +pub async fn update_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { + let sort_order = args.single::().unwrap_or_default(); + let style = args.single::().unwrap_or_default(); let guild = m.guild_id.unwrap(); let data = ctx.data.read().await; @@ -246,27 +257,7 @@ pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> Comma .await .ok(); drop(update_lock); - show_leaderboard(ctx, m, bm, sort_order).await -} - -#[command("leaderboard")] -#[aliases("lb", "bmranks", "br", "cc")] -#[usage = "[--score to sort by score, default to sort by pp]"] -#[description = "See the server's ranks on the last seen beatmap"] -#[max_args(1)] -#[only_in(guilds)] -pub async fn leaderboard(ctx: &Context, m: &Message, args: Args) -> CommandResult { - let sort_order = OrderBy::from(args.rest()); - - let data = ctx.data.read().await; - let bm = match get_beatmap(&*data, m.channel_id).await? { - Some(bm) => bm, - None => { - m.reply(&ctx, "No beatmap queried on this channel.").await?; - return Ok(()); - } - }; - show_leaderboard(ctx, m, bm, sort_order).await + show_leaderboard(ctx, m, bm, sort_order, style).await } async fn show_leaderboard( @@ -274,6 +265,7 @@ async fn show_leaderboard( m: &Message, bm: BeatmapWithMode, order: OrderBy, + style: ScoreListStyle, ) -> CommandResult { let data = ctx.data.read().await; @@ -381,6 +373,19 @@ async fn show_leaderboard( .await?; return Ok(()); } + + if let ScoreListStyle::Grid = style { + style + .display_scores( + scores.into_iter().map(|(_, _, a)| a).collect(), + mode, + ctx, + m, + ) + .await?; + return Ok(()); + } + paginate_reply_fn( move |page: u8, ctx: &Context, m: &mut Message| { const ITEMS_PER_PAGE: usize = 5;