osu: Allow returning a full file instead when requesting a table check/leaderboard

This commit is contained in:
Natsu Kagami 2025-03-10 15:51:52 +01:00
parent ed15406f51
commit a36fa87964
Signed by: nki
GPG key ID: 55A032EB38B49ADB
4 changed files with 215 additions and 146 deletions

View file

@ -6,7 +6,7 @@ use display::display_beatmapset;
use embeds::ScoreEmbedBuilder; use embeds::ScoreEmbedBuilder;
use link_parser::EmbedType; use link_parser::EmbedType;
use poise::{ChoiceParameter, CreateReply}; use poise::{ChoiceParameter, CreateReply};
use serenity::all::User; use serenity::all::{CreateAttachment, User};
use server_rank::get_leaderboard_from_embed; use server_rank::get_leaderboard_from_embed;
/// osu!-related command group. /// osu!-related command group.
@ -581,6 +581,7 @@ async fn leaderboard<U: HasOsuEnv>(
"Here are the top scores of **{}** on {}", "Here are the top scores of **{}** on {}",
guild.name, scoreboard_msg, guild.name, scoreboard_msg,
); );
let has_lazer_score = scores.iter().any(|v| v.score.mods.is_lazer);
match style { match style {
ScoreListStyle::Table => { ScoreListStyle::Table => {
@ -589,11 +590,30 @@ async fn leaderboard<U: HasOsuEnv>(
ctx.serenity_context(), ctx.serenity_context(),
reply, reply,
scores, scores,
has_lazer_score,
show_diff, show_diff,
sort.unwrap_or_default(), sort.unwrap_or_default(),
) )
.await?; .await?;
} }
ScoreListStyle::File => {
ctx.send(
CreateReply::default()
.content(header)
.attachment(CreateAttachment::bytes(
server_rank::rankings_to_table(
&scores,
0,
scores.len(),
has_lazer_score,
show_diff,
order,
),
"rankings.txt",
)),
)
.await?;
}
ScoreListStyle::Grid => { ScoreListStyle::Grid => {
let reply = ctx.reply(header).await?; let reply = ctx.reply(header).await?;
style style

View file

@ -16,6 +16,8 @@ mod scores {
Table, Table,
#[name = "List of Embeds"] #[name = "List of Embeds"]
Grid, Grid,
#[name = "Table File"]
File,
} }
impl Default for ScoreListStyle { impl Default for ScoreListStyle {
@ -46,6 +48,7 @@ mod scores {
) -> Result<()> { ) -> Result<()> {
match self { match self {
ScoreListStyle::Table => table::display_scores_table(scores, ctx, m).await, ScoreListStyle::Table => table::display_scores_table(scores, ctx, m).await,
ScoreListStyle::File => table::display_scores_as_file(scores, ctx, m).await,
ScoreListStyle::Grid => grid::display_scores_grid(scores, ctx, guild_id, m).await, ScoreListStyle::Grid => grid::display_scores_grid(scores, ctx, guild_id, m).await,
} }
} }
@ -152,7 +155,7 @@ mod scores {
use std::borrow::Cow; use std::borrow::Cow;
use pagination::paginate_with_first_message; use pagination::paginate_with_first_message;
use serenity::all::CreateActionRow; use serenity::all::{CreateActionRow, CreateAttachment};
use youmubot_prelude::table_format::Align::{Left, Right}; use youmubot_prelude::table_format::Align::{Left, Right};
use youmubot_prelude::table_format::{table_formatting, Align}; use youmubot_prelude::table_format::{table_formatting, Align};
@ -162,6 +165,29 @@ mod scores {
use crate::discord::{time_before_now, Beatmap, BeatmapInfo, OsuEnv}; use crate::discord::{time_before_now, Beatmap, BeatmapInfo, OsuEnv};
use crate::models::Score; use crate::models::Score;
pub async fn display_scores_as_file(
scores: Vec<Score>,
ctx: &Context,
mut on: impl CanEdit,
) -> Result<()> {
if scores.is_empty() {
on.apply_edit(CreateReply::default().content("No plays found"))
.await?;
return Ok(());
}
let p = Paginate {
env: ctx.data.read().await.get::<OsuEnv>().unwrap().clone(),
header: on.get_message().await?.content.clone(),
scores,
};
let content = p.to_table(0, p.scores.len()).await;
on.apply_edit(
CreateReply::default().attachment(CreateAttachment::bytes(content, "table.txt")),
)
.await?;
Ok(())
}
pub async fn display_scores_table( pub async fn display_scores_table(
scores: Vec<Score>, scores: Vec<Score>,
ctx: &Context, ctx: &Context,
@ -197,30 +223,13 @@ mod scores {
fn total_pages(&self) -> usize { fn total_pages(&self) -> usize {
(self.scores.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE (self.scores.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE
} }
}
const ITEMS_PER_PAGE: usize = 5; async fn to_table(&self, start: usize, end: usize) -> String {
let scores = &self.scores[start..end];
let meta_cache = &self.env.beatmaps;
let oppai = &self.env.oppai;
#[async_trait] let beatmaps = scores
impl pagination::Paginate for Paginate {
async fn render(
&mut self,
page: u8,
btns: Vec<CreateActionRow>,
) -> Result<Option<CreateReply>> {
let env = &self.env;
let meta_cache = &env.beatmaps;
let oppai = &env.oppai;
let page = page as usize;
let start = page * ITEMS_PER_PAGE;
let end = self.scores.len().min(start + ITEMS_PER_PAGE);
if start >= end {
return Ok(None);
}
let plays = &self.scores[start..end];
let beatmaps = plays
.iter() .iter()
.map(|play| async move { .map(|play| async move {
let beatmap = meta_cache.get_beatmap(play.beatmap_id, play.mode).await?; let beatmap = meta_cache.get_beatmap(play.beatmap_id, play.mode).await?;
@ -234,7 +243,7 @@ mod scores {
.map(|v| v.ok()) .map(|v| v.ok())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let pps = plays let pps = scores
.iter() .iter()
.map(|p| async move { .map(|p| async move {
match p.pp.map(|pp| format!("{:.2}", pp)) { match p.pp.map(|pp| format!("{:.2}", pp)) {
@ -257,7 +266,7 @@ mod scores {
let (beatmaps, pps) = future::join(beatmaps, pps).await; let (beatmaps, pps) = future::join(beatmaps, pps).await;
let ranks = plays let ranks = scores
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, p)| -> Cow<'static, str> { .map(|(i, p)| -> Cow<'static, str> {
@ -288,7 +297,7 @@ mod scores {
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(i, b)| { .map(|(i, b)| {
let play = &plays[i]; let play = &scores[i];
b.map(|(beatmap, info)| { b.map(|(beatmap, info)| {
format!( format!(
"[{:.1}*] {} - {} [{}] ({})", "[{:.1}*] {} - {} [{}] ({})",
@ -307,7 +316,7 @@ mod scores {
["#", "PP", "Acc", "Ranks", "Mods", "When", "Beatmap"]; ["#", "PP", "Acc", "Ranks", "Mods", "When", "Beatmap"];
const SCORE_ALIGNS: [Align; 7] = [Right, Right, Right, Right, Right, Right, Left]; const SCORE_ALIGNS: [Align; 7] = [Right, Right, Right, Right, Right, Right, Left];
let score_arr = plays let score_arr = scores
.iter() .iter()
.zip(beatmaps.iter()) .zip(beatmaps.iter())
.zip(ranks.iter().zip(pps.iter())) .zip(ranks.iter().zip(pps.iter()))
@ -325,9 +334,30 @@ mod scores {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let score_table = table_formatting(&SCORE_HEADERS, &SCORE_ALIGNS, score_arr); table_formatting(&SCORE_HEADERS, &SCORE_ALIGNS, score_arr)
}
}
const ITEMS_PER_PAGE: usize = 5;
#[async_trait]
impl pagination::Paginate for Paginate {
async fn render(
&mut self,
page: u8,
btns: Vec<CreateActionRow>,
) -> Result<Option<CreateReply>> {
let page = page as usize;
let start = page * ITEMS_PER_PAGE;
let end = self.scores.len().min(start + ITEMS_PER_PAGE);
if start >= end {
return Ok(None);
}
let plays = &self.scores[start..end];
let has_oppai = plays.iter().any(|p| p.pp.is_none()); let has_oppai = plays.iter().any(|p| p.pp.is_none());
let score_table = self.to_table(start, end).await;
let mut content = serenity::utils::MessageBuilder::new(); let mut content = serenity::utils::MessageBuilder::new();
content content
.push_line(&self.header) .push_line(&self.header)

View file

@ -413,7 +413,8 @@ pub fn handle_lb_button<'a>(
.content(format!("Here are the top scores on {}!", scoreboard_msg)), .content(format!("Here are the top scores on {}!", scoreboard_msg)),
) )
.await?; .await?;
display_rankings_table(ctx, reply, scores, show_diff, order).await?; let has_lazer_score = scores.iter().any(|s| s.score.mods.is_lazer);
display_rankings_table(ctx, reply, scores, has_lazer_score, show_diff, order).await?;
Ok(()) Ok(())
}) })
} }

View file

@ -10,7 +10,7 @@ use std::{
use chrono::DateTime; use chrono::DateTime;
use pagination::paginate_with_first_message; use pagination::paginate_with_first_message;
use serenity::{ use serenity::{
all::{GuildId, Member, PartialGuild}, all::{CreateAttachment, CreateMessage, GuildId, Member, PartialGuild},
framework::standard::{macros::command, Args, CommandResult}, framework::standard::{macros::command, Args, CommandResult},
model::channel::Message, model::channel::Message,
utils::MessageBuilder, utils::MessageBuilder,
@ -401,26 +401,41 @@ pub async fn show_leaderboard(ctx: &Context, msg: &Message, mut args: Args) -> C
return Ok(()); return Ok(());
} }
let header = format!(
"Here are the top scores of **{}** on {}",
guild.name(&ctx).unwrap(),
scoreboard_msg,
);
let has_lazer_score = scores.iter().any(|v| v.score.mods.is_lazer);
match style { match style {
ScoreListStyle::Table => { ScoreListStyle::Table => {
let reply = msg let reply = msg.reply(&ctx, header).await?;
.reply( display_rankings_table(ctx, reply, scores, has_lazer_score, show_diff, order).await?;
}
ScoreListStyle::File => {
msg.channel_id
.send_message(
&ctx, &ctx,
format!("⌛ Loading top scores on {}...", scoreboard_msg), CreateMessage::new()
.content(header)
.reference_message(msg)
.add_file(CreateAttachment::bytes(
rankings_to_table(
&scores,
0,
scores.len(),
has_lazer_score,
show_diff,
order,
),
"rankings.txt",
)),
) )
.await?; .await?;
display_rankings_table(ctx, reply, scores, show_diff, order).await?;
} }
ScoreListStyle::Grid => { ScoreListStyle::Grid => {
let reply = msg let reply = msg.reply(&ctx, header).await?;
.reply(
&ctx,
format!(
"Here are the top scores on {} of this server!",
scoreboard_msg
),
)
.await?;
style style
.display_scores( .display_scores(
scores.into_iter().map(|s| s.score).collect(), scores.into_iter().map(|s| s.score).collect(),
@ -600,30 +615,15 @@ pub async fn get_leaderboard_from_embed(
}) })
} }
pub async fn display_rankings_table( pub(crate) fn rankings_to_table(
ctx: &Context, scores: &[Ranking],
to: Message, start: usize,
scores: Vec<Ranking>, end: usize,
has_lazer_score: bool,
show_diff: bool, show_diff: bool,
order: OrderBy, order: OrderBy,
) -> Result<()> { ) -> String {
let has_lazer_score = scores.iter().any(|v| v.score.mods.is_lazer); assert!(start < end);
const ITEMS_PER_PAGE: usize = 5;
let total_len = scores.len();
let total_pages = (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
let header = Arc::new(to.content.clone());
paginate_with_first_message(
paginate_from_fn(move |page: u8, btns| {
let start = (page as usize) * ITEMS_PER_PAGE;
let end = (start + ITEMS_PER_PAGE).min(scores.len());
if start >= end {
return Box::pin(future::ready(Ok(None)));
}
let scores = scores[start..end].to_vec();
let header = header.clone();
Box::pin(async move {
let headers: [&'static str; 9] = [ let headers: [&'static str; 9] = [
"#", "#",
match order { match order {
@ -687,12 +687,7 @@ pub async fn display_rankings_table(
} else { } else {
beatmap.difficulty_name.clone() beatmap.difficulty_name.clone()
}; };
format!( format!("[{:.2}*] {} {}", star, trimmed_diff, score.mods.to_string())
"[{:.2}*] {} {}",
star,
trimmed_diff,
score.mods.to_string()
)
} else { } else {
score.mods.to_string() score.mods.to_string()
}, },
@ -706,10 +701,34 @@ pub async fn display_rankings_table(
}, },
) )
.collect::<Vec<_>>(); .collect::<Vec<_>>();
table_formatting(&headers, &aligns, &score_arr)
}
let score_table = table_formatting(&headers, &aligns, score_arr); pub async fn display_rankings_table(
ctx: &Context,
to: Message,
scores: Vec<Ranking>,
has_lazer_score: bool,
show_diff: bool,
order: OrderBy,
) -> Result<()> {
const ITEMS_PER_PAGE: usize = 5;
let total_len = scores.len();
let total_pages = (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
let header = to.content.clone();
paginate_with_first_message(
paginate_from_fn(move |page: u8, btns| {
let start = (page as usize) * ITEMS_PER_PAGE;
let end = (start + ITEMS_PER_PAGE).min(scores.len());
if start >= end {
return Box::pin(future::ready(Ok(None)));
}
let scores = scores[start..end].to_vec();
let score_table =
rankings_to_table(&scores, start, end, has_lazer_score, show_diff, order);
let content = MessageBuilder::new() let content = MessageBuilder::new()
.push_line(header.as_ref()) .push_line(&header)
.push_line(score_table) .push_line(score_table)
.push_line(format!( .push_line(format!(
"Page **{}**/**{}**. Not seeing your scores? Run `osu check` to update.", "Page **{}**/**{}**. Not seeing your scores? Run `osu check` to update.",
@ -718,10 +737,9 @@ pub async fn display_rankings_table(
)) ))
.build(); .build();
Ok(Some( Box::pin(future::ready(Ok(Some(
CreateReply::default().content(content).components(btns), CreateReply::default().content(content).components(btns),
)) ))))
})
}) })
.with_page_count(total_pages), .with_page_count(total_pages),
ctx, ctx,