Use grid throughout the commands

This commit is contained in:
Natsu Kagami 2021-04-28 14:00:53 +09:00
parent ba8b835cc2
commit f27939546e
Signed by: nki
GPG key ID: 7306B3D3C3AD6E51
4 changed files with 188 additions and 60 deletions

View file

@ -1,7 +1,126 @@
pub use beatmapset::display_beatmapset; pub use beatmapset::display_beatmapset;
pub use scores::table::display_scores_table; pub use scores::ScoreListStyle;
mod scores { 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<Self, Self::Err> {
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<Score>,
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<Score>,
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<Score>,
mode: Mode,
}
#[async_trait]
impl pagination::Paginate for Paginate {
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
let data = ctx.data.read().await;
let client = data.get::<crate::discord::OsuClient>().unwrap();
let osu = data.get::<BeatmapMetaCache>().unwrap();
let beatmap_cache = data.get::<BeatmapCache>().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<usize> {
Some(self.scores.len())
}
}
}
pub mod table { pub mod table {
use crate::discord::{Beatmap, BeatmapCache, BeatmapInfo, BeatmapMetaCache}; use crate::discord::{Beatmap, BeatmapCache, BeatmapInfo, BeatmapMetaCache};
use crate::models::{Mode, Score}; use crate::models::{Mode, Score};

View file

@ -169,6 +169,7 @@ pub(crate) struct ScoreEmbedBuilder<'a> {
u: &'a User, u: &'a User,
top_record: Option<u8>, top_record: Option<u8>,
world_record: Option<u16>, world_record: Option<u16>,
footer: Option<String>,
} }
impl<'a> ScoreEmbedBuilder<'a> { impl<'a> ScoreEmbedBuilder<'a> {
@ -180,6 +181,10 @@ impl<'a> ScoreEmbedBuilder<'a> {
self.world_record = Some(rank); self.world_record = Some(rank);
self self
} }
pub fn footer(&mut self, footer: impl Into<String>) -> &mut Self {
self.footer = Some(footer.into());
self
}
} }
pub(crate) fn score_embed<'a>( pub(crate) fn score_embed<'a>(
@ -195,12 +200,13 @@ pub(crate) fn score_embed<'a>(
u, u,
top_record: None, top_record: None,
world_record: None, world_record: None,
footer: None,
} }
} }
impl<'a> ScoreEmbedBuilder<'a> { impl<'a> ScoreEmbedBuilder<'a> {
#[allow(clippy::many_single_char_names)] #[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 mode = self.bm.mode();
let b = &self.bm.0; let b = &self.bm.0;
let s = self.s; let s = self.s;
@ -358,8 +364,12 @@ impl<'a> ScoreEmbedBuilder<'a> {
) )
.field("Map stats", diff.format_info(mode, s.mods, b), false) .field("Map stats", diff.format_info(mode, s.mods, b), false)
.timestamp(&s.date); .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 { 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 m
} }

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
discord::beatmap_cache::BeatmapMetaCache, discord::beatmap_cache::BeatmapMetaCache,
discord::display::display_scores_table as list_scores, discord::display::ScoreListStyle,
discord::oppai_cache::{BeatmapCache, BeatmapInfo}, discord::oppai_cache::{BeatmapCache, BeatmapInfo},
models::{Beatmap, Mode, Mods, User}, models::{Beatmap, Mode, Mods, User},
request::UserID, request::UserID,
@ -32,7 +32,7 @@ use db::OsuUser;
use db::{OsuLastBeatmap, OsuSavedUsers, OsuUserBests}; use db::{OsuLastBeatmap, OsuSavedUsers, OsuUserBests};
use embeds::{beatmap_embed, score_embed, user_embed}; use embeds::{beatmap_embed, score_embed, user_embed};
pub use hook::hook; 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. /// The osu! client.
pub(crate) struct OsuClient; pub(crate) struct OsuClient;
@ -108,7 +108,6 @@ pub fn setup(
check, check,
top, top,
server_rank, server_rank,
leaderboard,
update_leaderboard update_leaderboard
)] )]
#[default_command(std)] #[default_command(std)]
@ -315,13 +314,15 @@ impl FromStr for Nth {
} }
#[command] #[command]
#[aliases("rs", "rc")]
#[description = "Gets an user's recent play"] #[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"] #[example = "#1 / taiko / natsukagami"]
#[max_args(3)] #[max_args(4)]
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 data = ctx.data.read().await; let data = ctx.data.read().await;
let nth = args.single::<Nth>().unwrap_or(Nth::All); let nth = args.single::<Nth>().unwrap_or(Nth::All);
let style = args.single::<ScoreListStyle>().unwrap_or_default();
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0; let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg).await?; let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg).await?;
@ -361,7 +362,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
let plays = osu let plays = osu
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50)) .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))
.await?; .await?;
list_scores(plays, mode, ctx, msg).await?; style.display_scores(plays, mode, ctx, msg).await?;
} }
} }
Ok(()) Ok(())
@ -433,9 +434,9 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
#[command] #[command]
#[aliases("c", "chk")] #[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."] #[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 { pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await; let data = ctx.data.read().await;
let bm = cache::get_beatmap(&*data, msg.channel_id).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) => { Some(bm) => {
let b = &bm.0; let b = &bm.0;
let m = bm.1; let m = bm.1;
let style = args.single::<ScoreListStyle>().unwrap_or_default();
let username_arg = args.single::<UsernameArg>().ok(); let username_arg = args.single::<UsernameArg>().ok();
let user_id = match username_arg.as_ref() { let user_id = match username_arg.as_ref() {
Some(UsernameArg::Tagged(v)) => Some(*v), 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 user = to_user_id_query(username_arg, &*data, msg).await?;
let osu = data.get::<OsuClient>().unwrap(); let osu = data.get::<OsuClient>().unwrap();
let oppai = data.get::<BeatmapCache>().unwrap();
let content = oppai.get_beatmap(b.beatmap_id).await?;
let user = osu let user = osu
.user(user, |f| f) .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?; 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 { if let Some(user_id) = user_id {
// Save to database // Save to database
data.get::<OsuUserBests>() data.get::<OsuUserBests>()
.unwrap() .unwrap()
.save(user_id, m, scores) .save(user_id, m, scores.clone())
.await .await
.pls_ok(); .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] #[command]
#[description = "Get the n-th top record of an user."] #[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]"] #[usage = "#[n-th = --all] / [style (table or grid) = --table] / [mode (std, taiko, catch, mania)] = std / [username or user_id = your saved user id]"]
#[example = "taiko / #2 / natsukagami"] #[example = "#2 / taiko / natsukagami"]
#[max_args(3)] #[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 data = ctx.data.read().await; let data = ctx.data.read().await;
let nth = args.single::<Nth>().unwrap_or(Nth::All); let nth = args.single::<Nth>().unwrap_or(Nth::All);
let style = args.single::<ScoreListStyle>().unwrap_or_default();
let mode = args let mode = args
.single::<ModeArg>() .single::<ModeArg>()
.map(|ModeArg(t)| t) .map(|ModeArg(t)| t)
@ -555,7 +549,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
let plays = osu let plays = osu
.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?;
list_scores(plays, mode, ctx, msg).await?; style.display_scores(plays, mode, ctx, msg).await?;
} }
} }
Ok(()) Ok(())

View file

@ -5,6 +5,7 @@ use super::{
}; };
use crate::{ use crate::{
discord::{ discord::{
display::ScoreListStyle,
oppai_cache::{BeatmapCache, OppaiAccuracy}, oppai_cache::{BeatmapCache, OppaiAccuracy},
BeatmapWithMode, BeatmapWithMode,
}, },
@ -151,23 +152,33 @@ enum OrderBy {
Score, Score,
} }
impl From<&str> for OrderBy { impl Default for OrderBy {
fn from(s: &str) -> Self { fn default() -> Self {
if s == "--score" { Self::Score
Self::Score }
} else { }
Self::PP
impl std::str::FromStr for OrderBy {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"--score" => Ok(OrderBy::Score),
"--pp" => Ok(OrderBy::PP),
_ => Err(Error::msg("unknown value")),
} }
} }
} }
#[command("updatelb")] #[command("leaderboard")]
#[description = "Update the leaderboard on the last seen beatmap"] #[aliases("lb", "bmranks", "br", "cc", "updatelb")]
#[usage = "[--score to sort by score, default to sort by pp]"] #[usage = "[--score to sort by score, default to sort by pp] / [--table to show a table, --grid to show score by score]"]
#[max_args(1)] #[description = "See the server's ranks on the last seen beatmap"]
#[max_args(2)]
#[only_in(guilds)] #[only_in(guilds)]
pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> CommandResult { pub async fn update_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
let sort_order = OrderBy::from(args.rest()); let sort_order = args.single::<OrderBy>().unwrap_or_default();
let style = args.single::<ScoreListStyle>().unwrap_or_default();
let guild = m.guild_id.unwrap(); let guild = m.guild_id.unwrap();
let data = ctx.data.read().await; let data = ctx.data.read().await;
@ -246,27 +257,7 @@ pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> Comma
.await .await
.ok(); .ok();
drop(update_lock); drop(update_lock);
show_leaderboard(ctx, m, bm, sort_order).await show_leaderboard(ctx, m, bm, sort_order, style).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
} }
async fn show_leaderboard( async fn show_leaderboard(
@ -274,6 +265,7 @@ async fn show_leaderboard(
m: &Message, m: &Message,
bm: BeatmapWithMode, bm: BeatmapWithMode,
order: OrderBy, order: OrderBy,
style: ScoreListStyle,
) -> CommandResult { ) -> CommandResult {
let data = ctx.data.read().await; let data = ctx.data.read().await;
@ -381,6 +373,19 @@ async fn show_leaderboard(
.await?; .await?;
return Ok(()); 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( paginate_reply_fn(
move |page: u8, ctx: &Context, m: &mut Message| { move |page: u8, ctx: &Context, m: &mut Message| {
const ITEMS_PER_PAGE: usize = 5; const ITEMS_PER_PAGE: usize = 5;