pagination: make editing generic over a CanEdit interface

This commit is contained in:
Natsu Kagami 2025-02-20 22:54:38 +01:00
parent 5fde2f343a
commit a35563801d
Signed by: nki
GPG key ID: 55A032EB38B49ADB
11 changed files with 233 additions and 207 deletions

View file

@ -294,11 +294,14 @@ async fn handle_listing<U: HasOsuEnv>(
listing_kind,
user.mention()
))
.await?
.into_message()
.await?;
style
.display_scores(plays, ctx.serenity_context(), ctx.guild_id(), reply)
.display_scores(
plays,
ctx.clone().serenity_context(),
ctx.guild_id(),
(reply, ctx),
)
.await?;
}
}
@ -368,16 +371,14 @@ async fn beatmap<U: HasOsuEnv>(
let msg = ctx
.clone()
.reply(format!("Information for {}", b0.beatmapset_mention()))
.await?
.into_message()
.await?;
display_beatmapset(
ctx.serenity_context().clone(),
ctx.clone().serenity_context(),
vec,
mode,
mods,
ctx.guild_id(),
msg,
(msg, ctx),
)
.await?;
}
@ -470,8 +471,6 @@ async fn check<U: HasOsuEnv>(
args.user.mention(),
display
))
.await?
.into_message()
.await?;
let style = style.unwrap_or(if scores.len() <= 5 {
@ -481,7 +480,12 @@ async fn check<U: HasOsuEnv>(
});
style
.display_scores(scores, ctx.serenity_context(), ctx.guild_id(), msg)
.display_scores(
scores,
ctx.clone().serenity_context(),
ctx.guild_id(),
(msg, ctx),
)
.await?;
Ok(())
@ -577,13 +581,13 @@ async fn leaderboard<U: HasOsuEnv>(
.await?;
}
ScoreListStyle::Grid => {
let reply = ctx.reply(header).await?.into_message().await?;
let reply = ctx.reply(header).await?;
style
.display_scores(
scores.into_iter().map(|s| s.score).collect(),
ctx.serenity_context(),
Some(guild.id),
reply,
(reply, ctx),
)
.await?;
}

View file

@ -3,7 +3,7 @@ pub use scores::ScoreListStyle;
mod scores {
use poise::ChoiceParameter;
use serenity::{all::GuildId, model::channel::Message};
use serenity::all::GuildId;
use youmubot_prelude::*;
@ -42,7 +42,7 @@ mod scores {
scores: Vec<Score>,
ctx: &Context,
guild_id: Option<GuildId>,
m: Message,
m: impl CanEdit,
) -> Result<()> {
match self {
ScoreListStyle::Table => table::display_scores_table(scores, ctx, m).await,
@ -54,8 +54,6 @@ mod scores {
mod grid {
use pagination::paginate_with_first_message;
use serenity::all::{CreateActionRow, GuildId};
use serenity::builder::EditMessage;
use serenity::model::channel::Message;
use youmubot_prelude::*;
@ -67,16 +65,23 @@ mod scores {
scores: Vec<Score>,
ctx: &Context,
guild_id: Option<GuildId>,
mut on: Message,
mut on: impl CanEdit,
) -> Result<()> {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let channel_id = on.get_message().await?.channel_id;
if scores.is_empty() {
on.edit(&ctx, EditMessage::new().content("No plays found"))
on.apply_edit(CreateReply::default().content("No plays found"))
.await?;
return Ok(());
}
paginate_with_first_message(
Paginate { scores, guild_id },
Paginate {
env,
scores,
guild_id,
channel_id,
},
ctx,
on,
std::time::Duration::from_secs(60),
@ -86,8 +91,10 @@ mod scores {
}
pub struct Paginate {
env: OsuEnv,
scores: Vec<Score>,
guild_id: Option<GuildId>,
channel_id: serenity::all::ChannelId,
}
#[async_trait]
@ -95,11 +102,9 @@ mod scores {
async fn render(
&mut self,
page: u8,
ctx: &Context,
msg: &Message,
btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>> {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
) -> Result<Option<CreateReply>> {
let env = &self.env;
let page = page as usize;
let score = &self.scores[page];
@ -120,9 +125,9 @@ mod scores {
.await?
.ok_or_else(|| Error::msg("user not found"))?;
save_beatmap(&env, msg.channel_id, &bm).await?;
save_beatmap(&env, self.channel_id, &bm).await?;
Ok(Some(
EditMessage::new()
CreateReply::default()
.embed({
crate::discord::embeds::score_embed(score, &bm, &content, &user)
.footer(format!("Page {}/{}", page + 1, self.scores.len()))
@ -148,8 +153,6 @@ mod scores {
use pagination::paginate_with_first_message;
use serenity::all::CreateActionRow;
use serenity::builder::EditMessage;
use serenity::model::channel::Message;
use youmubot_prelude::table_format::Align::{Left, Right};
use youmubot_prelude::table_format::{table_formatting, Align};
@ -162,17 +165,18 @@ mod scores {
pub async fn display_scores_table(
scores: Vec<Score>,
ctx: &Context,
mut on: Message,
mut on: impl CanEdit,
) -> Result<()> {
if scores.is_empty() {
on.edit(&ctx, EditMessage::new().content("No plays found"))
on.apply_edit(CreateReply::default().content("No plays found"))
.await?;
return Ok(());
}
paginate_with_first_message(
Paginate {
header: on.content.clone(),
env: ctx.data.read().await.get::<OsuEnv>().unwrap().clone(),
header: on.get_message().await?.content.clone(),
scores,
},
ctx,
@ -184,6 +188,7 @@ mod scores {
}
pub struct Paginate {
env: OsuEnv,
header: String,
scores: Vec<Score>,
}
@ -201,11 +206,9 @@ mod scores {
async fn render(
&mut self,
page: u8,
ctx: &Context,
_: &Message,
btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>> {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
) -> Result<Option<CreateReply>> {
let env = &self.env;
let meta_cache = &env.beatmaps;
let oppai = &env.oppai;
@ -331,7 +334,9 @@ mod scores {
.push_line("[?] means pp was predicted by oppai-rs.")
.build();
Ok(Some(EditMessage::new().content(content).components(btns)))
Ok(Some(
CreateReply::default().content(content).components(btns),
))
}
fn len(&self) -> Option<usize> {
@ -344,8 +349,8 @@ mod scores {
mod beatmapset {
use serenity::{
all::{CreateActionRow, CreateButton, GuildId},
builder::{CreateEmbedFooter, EditMessage},
model::channel::{Message, ReactionType},
builder::CreateEmbedFooter,
model::channel::ReactionType,
};
use youmubot_prelude::*;
@ -363,12 +368,12 @@ mod beatmapset {
const SHOW_ALL: &str = "youmubot_osu::discord::display::show_all";
pub async fn display_beatmapset(
ctx: Context,
ctx: &Context,
mut beatmapset: Vec<Beatmap>,
mode: Option<Mode>,
mods: Option<UnparsedMods>,
guild_id: Option<GuildId>,
target: Message,
target: impl CanEdit,
) -> Result<bool> {
assert!(!beatmapset.is_empty(), "Beatmapset should not be empty");
@ -381,6 +386,8 @@ mod beatmapset {
});
let p = Paginate {
env: ctx.data.read().await.get::<OsuEnv>().unwrap().clone(),
channel_id: target.get_message().await?.channel_id,
infos: vec![None; beatmapset.len()],
maps: beatmapset,
mode,
@ -389,20 +396,20 @@ mod beatmapset {
};
let ctx = ctx.clone();
spawn_future(async move {
pagination::paginate_with_first_message(
p,
&ctx,
target,
std::time::Duration::from_secs(60),
)
.await
.pls_ok();
});
pagination::paginate_with_first_message(
p,
&ctx,
target,
std::time::Duration::from_secs(60),
)
.await
.pls_ok();
Ok(true)
}
struct Paginate {
env: OsuEnv,
channel_id: serenity::all::ChannelId,
maps: Vec<Beatmap>,
infos: Vec<Option<BeatmapInfoWithPP>>,
mode: Option<Mode>,
@ -411,15 +418,9 @@ mod beatmapset {
}
impl Paginate {
async fn get_beatmap_info(
&self,
ctx: &Context,
b: &Beatmap,
mods: &Mods,
) -> Result<BeatmapInfoWithPP> {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
env.oppai
async fn get_beatmap_info(&self, b: &Beatmap, mods: &Mods) -> Result<BeatmapInfoWithPP> {
self.env
.oppai
.get_beatmap(b.beatmap_id)
.await
.map(move |v| v.get_possible_pp_with(b.mode.with_override(self.mode), &mods))
@ -435,14 +436,12 @@ mod beatmapset {
async fn render(
&mut self,
page: u8,
ctx: &Context,
msg: &Message,
btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>> {
) -> Result<Option<CreateReply>> {
let page = page as usize;
if page == self.maps.len() {
return Ok(Some(
EditMessage::new()
CreateReply::default()
.embed(crate::discord::embeds::beatmapset_embed(
&self.maps[..],
self.mode,
@ -464,21 +463,20 @@ mod beatmapset {
let info = match &self.infos[page] {
Some(info) => info,
None => {
let info = self.get_beatmap_info(ctx, map, &mods).await?;
let info = self.get_beatmap_info(map, &mods).await?;
self.infos[page].insert(info)
}
};
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
save_beatmap(
&env,
msg.channel_id,
&self.env,
self.channel_id,
&BeatmapWithMode(map.clone(), self.mode),
)
.await
.pls_ok();
Ok(Some(
EditMessage::new().embed(
CreateReply::default().embed(
crate::discord::embeds::beatmap_embed(
map,
self.mode.unwrap_or(map.mode),
@ -512,16 +510,16 @@ mod beatmapset {
async fn handle_reaction(
&mut self,
page: u8,
ctx: &Context,
message: &mut serenity::model::channel::Message,
_ctx: &Context,
message: &mut impl CanEdit,
reaction: &str,
) -> Result<Option<u8>> {
// Render the old style.
if reaction == SHOW_ALL {
pagination::do_render(self, self.maps.len() as u8, ctx, message).await?;
pagination::do_render(self, self.maps.len() as u8, message).await?;
return Ok(Some(self.maps.len() as u8));
}
pagination::handle_pagination_reaction(page, self, ctx, message, reaction)
pagination::handle_pagination_reaction(page, self, message, reaction)
.await
.map(Some)
}

View file

@ -2,9 +2,7 @@ use std::sync::Arc;
use futures_util::stream::FuturesOrdered;
use pagination::paginate_from_fn;
use serenity::{
all::EditMessage, builder::CreateMessage, model::channel::Message, utils::MessageBuilder,
};
use serenity::{builder::CreateMessage, model::channel::Message, utils::MessageBuilder};
use stream::Stream;
use youmubot_prelude::*;
@ -192,16 +190,16 @@ pub fn dot_osu_hook<'a>(
} else {
let osu_embeds = Arc::new(osu_embeds);
paginate_reply(
paginate_from_fn(|page, _, _, btns| {
paginate_from_fn(|page, btns| {
let osu_embeds = osu_embeds.clone();
Box::pin(async move {
let (embed, attachments) = &osu_embeds[page as usize];
let mut edit = EditMessage::new()
let mut edit = CreateReply::default()
.content(format!("Attached beatmaps ({}/{})", page + 1, embed_len))
.embed(embed.clone())
.components(btns);
for att in attachments {
edit = edit.new_attachment(att.clone());
edit = edit.attachment(att.clone());
}
Ok(Some(edit))
})
@ -334,12 +332,12 @@ async fn handle_beatmapset<'a, 'b>(
)
.await?;
crate::discord::display::display_beatmapset(
ctx.clone(),
ctx,
beatmaps,
mode,
None,
reply_to.guild_id,
reply,
(reply, ctx),
)
.await
.pls_ok();

View file

@ -95,27 +95,22 @@ pub fn handle_check_button<'a>(
return Ok(());
}
let reply = comp
.create_followup(
&ctx,
CreateInteractionResponseFollowup::new().content(format!(
"Here are the scores by [`{}`](<https://osu.ppy.sh/users/{}>) on {}!",
user.username,
user.id,
embed.mention()
)),
)
.await?;
comp.create_followup(
&ctx,
CreateInteractionResponseFollowup::new().content(format!(
"Here are the scores by [`{}`](<https://osu.ppy.sh/users/{}>) on {}!",
user.username,
user.id,
embed.mention()
)),
)
.await?;
let ctx = ctx.clone();
let guild_id = comp.guild_id;
spawn_future(async move {
ScoreListStyle::Grid
.display_scores(scores, &ctx, guild_id, reply)
.await
.pls_ok();
});
ScoreListStyle::Grid
.display_scores(scores, &ctx, guild_id, (comp, ctx))
.await
.pls_ok();
Ok(())
})
}
@ -326,24 +321,21 @@ async fn handle_last_req(
.await
.unwrap();
let content_type = format!("Information for {}", embed.mention());
match embed {
EmbedType::Beatmapset(beatmapset, mode) => {
let reply = comp
.create_followup(
&ctx,
CreateInteractionResponseFollowup::new().content(format!(
"Beatmapset `{}`",
beatmapset[0].beatmapset_mention()
)),
)
.await?;
comp.create_followup(
&ctx,
CreateInteractionResponseFollowup::new().content(content_type),
)
.await?;
super::display::display_beatmapset(
ctx.clone(),
ctx,
beatmapset,
mode,
None,
comp.guild_id,
reply,
(comp, ctx),
)
.await?;
return Ok(());
@ -357,7 +349,7 @@ async fn handle_last_req(
comp.create_followup(
&ctx,
serenity::all::CreateInteractionResponseFollowup::new()
.content(format!("Information for beatmap {}", b.mention(m, &mods)))
.content(content_type)
.embed(beatmap_embed(&*b, m.unwrap_or(b.mode), &mods, &info))
.components(vec![beatmap_components(m.unwrap_or(b.mode), comp.guild_id)]),
)

View file

@ -689,7 +689,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
)
.await?;
style
.display_scores(plays, ctx, reply.guild_id, reply)
.display_scores(plays, ctx, reply.guild_id, (reply, ctx))
.await?;
}
Nth::Nth(nth) => {
@ -762,7 +762,7 @@ pub async fn pins(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
)
.await?;
style
.display_scores(plays, ctx, reply.guild_id, reply)
.display_scores(plays, ctx, reply.guild_id, (reply, ctx))
.await?;
}
Nth::Nth(nth) => {
@ -966,7 +966,7 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
let reply = msg
.reply(&ctx, format!("Information for {}", content_type))
.await?;
display::display_beatmapset(ctx.clone(), beatmaps, mode, umods, msg.guild_id, reply)
display::display_beatmapset(ctx, beatmaps, mode, umods, msg.guild_id, (reply, ctx))
.await?;
}
}
@ -1018,7 +1018,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
)
.await?;
style
.display_scores(scores, ctx, msg.guild_id, reply)
.display_scores(scores, ctx, msg.guild_id, (reply, ctx))
.await?;
Ok(())
@ -1130,7 +1130,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
)
.await?;
style
.display_scores(plays, ctx, msg.guild_id, reply)
.display_scores(plays, ctx, msg.guild_id, (reply, ctx))
.await?;
}
}

View file

@ -11,7 +11,6 @@ use chrono::DateTime;
use pagination::paginate_with_first_message;
use serenity::{
all::{GuildId, Member, PartialGuild},
builder::EditMessage,
framework::standard::{macros::command, Args, CommandResult},
model::channel::Message,
utils::MessageBuilder,
@ -236,7 +235,7 @@ where
let total_len = users.len();
let total_pages = (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
paginate_with_first_message(
paginate_from_fn(move |page: u8, _: &Context, _: &Message, btns| {
paginate_from_fn(move |page: u8, btns| {
let header = header.clone();
use Align::*;
let users = users.clone();
@ -317,12 +316,14 @@ where
last_update.format("<t:%s:R>"),
))
.build();
Ok(Some(EditMessage::new().content(content).components(btns)))
Ok(Some(
CreateReply::default().content(content).components(btns),
))
})
})
.with_page_count(total_pages),
ctx,
msg,
(msg, ctx),
std::time::Duration::from_secs(60),
)
.await?;
@ -425,7 +426,7 @@ pub async fn show_leaderboard(ctx: &Context, msg: &Message, mut args: Args) -> C
scores.into_iter().map(|s| s.score).collect(),
ctx,
Some(guild),
reply,
(reply, ctx),
)
.await?;
}
@ -614,7 +615,7 @@ pub async fn display_rankings_table(
let header = Arc::new(to.content.clone());
paginate_with_first_message(
paginate_from_fn(move |page: u8, _, _, btns| {
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 {
@ -717,12 +718,14 @@ pub async fn display_rankings_table(
))
.build();
Ok(Some(EditMessage::new().content(content).components(btns)))
Ok(Some(
CreateReply::default().content(content).components(btns),
))
})
})
.with_page_count(total_pages),
ctx,
to,
(to, ctx),
std::time::Duration::from_secs(60),
)
.await?;