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

@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
use codeforces::Contest; use codeforces::Contest;
use pagination::paginate_from_fn; use pagination::paginate_from_fn;
use serenity::{ use serenity::{
builder::{CreateMessage, EditMessage}, builder::CreateMessage,
framework::standard::{ framework::standard::{
macros::{command, group}, macros::{command, group},
Args, CommandResult, Args, CommandResult,
@ -180,7 +180,7 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
let last_updated = ranks.iter().map(|(_, cfu)| cfu.last_update).min().unwrap(); let last_updated = ranks.iter().map(|(_, cfu)| cfu.last_update).min().unwrap();
paginate_reply( paginate_reply(
paginate_from_fn(move |page, _, _, btns| { paginate_from_fn(move |page, btns| {
use Align::*; use Align::*;
let ranks = ranks.clone(); let ranks = ranks.clone();
Box::pin(async move { Box::pin(async move {
@ -222,7 +222,9 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
)) ))
.build(); .build();
Ok(Some(EditMessage::new().content(content).components(btns))) Ok(Some(
CreateReply::default().content(content).components(btns),
))
}) })
}) })
.with_page_count(total_pages), .with_page_count(total_pages),
@ -317,7 +319,7 @@ pub(crate) async fn contest_rank_table(
let ranks = Arc::new(ranks); let ranks = Arc::new(ranks);
paginate_reply( paginate_reply(
paginate_from_fn(move |page, _, _, btns| { paginate_from_fn(move |page, btns| {
let contest = contest.clone(); let contest = contest.clone();
let problems = problems.clone(); let problems = problems.clone();
let ranks = ranks.clone(); let ranks = ranks.clone();
@ -388,7 +390,9 @@ pub(crate) async fn contest_rank_table(
.push_line(format!("Page **{}/{}**", page + 1, total_pages)) .push_line(format!("Page **{}/{}**", page + 1, total_pages))
.build(); .build();
Ok(Some(EditMessage::new().content(content).components(btns))) Ok(Some(
CreateReply::default().content(content).components(btns),
))
}) })
}) })
.with_page_count(total_pages), .with_page_count(total_pages),

View file

@ -1,5 +1,4 @@
use serenity::{ use serenity::{
builder::EditMessage,
framework::standard::{macros::command, Args, CommandResult}, framework::standard::{macros::command, Args, CommandResult},
model::{ model::{
channel::{Message, ReactionType}, channel::{Message, ReactionType},
@ -44,7 +43,7 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE; let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE;
paginate_reply( paginate_reply(
paginate_from_fn(|page, _, _, btns| { paginate_from_fn(|page, btns| {
let roles = roles.clone(); let roles = roles.clone();
Box::pin(async move { Box::pin(async move {
let page = page as usize; let page = page as usize;
@ -77,7 +76,9 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
.push_line(format!("Page **{}/{}**", page + 1, pages)) .push_line(format!("Page **{}/{}**", page + 1, pages))
.build(); .build();
Ok(Some(EditMessage::new().content(content).components(btns))) Ok(Some(
CreateReply::default().content(content).components(btns),
))
}) })
}) })
.with_page_count(pages), .with_page_count(pages),

View file

@ -1,5 +1,4 @@
use serde::Deserialize; use serde::Deserialize;
use serenity::builder::EditMessage;
use serenity::framework::standard::CommandError as Error; use serenity::framework::standard::CommandError as Error;
use serenity::{ use serenity::{
framework::standard::{ framework::standard::{
@ -66,7 +65,7 @@ async fn message_command(
} }
let images = std::sync::Arc::new(images); let images = std::sync::Arc::new(images);
paginate_reply( paginate_reply(
paginate_from_fn(|page, _, _, btns| { paginate_from_fn(|page, btns| {
let images = images.clone(); let images = images.clone();
Box::pin(async move { Box::pin(async move {
let page = page as usize; let page = page as usize;
@ -74,7 +73,7 @@ async fn message_command(
Ok(None) Ok(None)
} else { } else {
Ok(Some( Ok(Some(
EditMessage::new() CreateReply::default()
.content(format!( .content(format!(
"[🖼️ **{}/{}**] Here's the image you requested!\n\n{}", "[🖼️ **{}/{}**] Here's the image you requested!\n\n{}",
page + 1, page + 1,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,7 +18,8 @@ pub use debugging_ok::OkPrint;
pub use flags::Flags; pub use flags::Flags;
pub use hook::Hook; pub use hook::Hook;
pub use member_cache::MemberCache; pub use member_cache::MemberCache;
pub use pagination::{paginate, paginate_from_fn, paginate_reply, Paginate}; pub use pagination::{paginate, paginate_from_fn, paginate_reply, CanEdit, Paginate};
pub use poise::CreateReply;
pub mod announcer; pub mod announcer;
pub mod args; pub mod args;

View file

@ -1,9 +1,10 @@
use crate::{Context, OkPrint, Result}; use crate::{CmdContext, Context, OkPrint, Result};
use futures_util::future::Future; use futures_util::future::Future;
use poise::{CreateReply, ReplyHandle};
use serenity::{ use serenity::{
all::{ all::{
CreateActionRow, CreateButton, CreateInteractionResponse, EditMessage, Interaction, ComponentInteraction, CreateActionRow, CreateButton, CreateInteractionResponse,
MessageId, EditInteractionResponse, EditMessage, Interaction, MessageId,
}, },
builder::CreateMessage, builder::CreateMessage,
model::{ model::{
@ -15,28 +16,68 @@ use serenity::{
use std::{convert::TryFrom, sync::Arc}; use std::{convert::TryFrom, sync::Arc};
use tokio::time as tokio_time; use tokio::time as tokio_time;
const ARROW_RIGHT: &str = "➡️"; // const ARROW_RIGHT: &str = "➡️";
const ARROW_LEFT: &str = "⬅️"; // const ARROW_LEFT: &str = "⬅️";
const REWIND: &str = ""; // const REWIND: &str = "⏪";
const FAST_FORWARD: &str = ""; // const FAST_FORWARD: &str = "⏩";
const NEXT: &str = "youmubot_pagination_next"; const NEXT: &str = "youmubot_pagination_next";
const PREV: &str = "youmubot_pagination_prev"; const PREV: &str = "youmubot_pagination_prev";
const FAST_NEXT: &str = "youmubot_pagination_fast_next"; const FAST_NEXT: &str = "youmubot_pagination_fast_next";
const FAST_PREV: &str = "youmubot_pagination_fast_prev"; const FAST_PREV: &str = "youmubot_pagination_fast_prev";
pub trait CanEdit: Send {
fn get_message(&self) -> impl Future<Output = Result<Message>> + Send;
fn apply_edit(&mut self, edit: CreateReply) -> impl Future<Output = Result<()>> + Send;
}
impl<'a> CanEdit for (Message, &'a Context) {
async fn get_message(&self) -> Result<Message> {
Ok(self.0.clone())
}
async fn apply_edit(&mut self, edit: CreateReply) -> Result<()> {
self.0
.edit(&self.1, edit.to_prefix_edit(EditMessage::new()))
.await?;
Ok(())
}
}
impl<'a, 'b> CanEdit for (&'a ComponentInteraction, &'b Context) {
async fn get_message(&self) -> Result<Message> {
Ok(self.0.get_response(&self.1.http).await?)
}
async fn apply_edit(&mut self, edit: CreateReply) -> Result<()> {
self.0
.edit_response(
&self.1,
edit.to_slash_initial_response_edit(EditInteractionResponse::new()),
)
.await?;
Ok(())
}
}
impl<'a, 'e, Env: Send + Sync> CanEdit for (ReplyHandle<'a>, CmdContext<'e, Env>) {
async fn get_message(&self) -> Result<Message> {
Ok(self.0.message().await?.into_owned())
}
async fn apply_edit(&mut self, edit: CreateReply) -> Result<()> {
self.0.edit(self.1, edit).await?;
Ok(())
}
}
/// A trait that provides the implementation of a paginator. /// A trait that provides the implementation of a paginator.
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait Paginate: Send + Sized { pub trait Paginate: Send + Sized {
/// Render the given page. /// Render the given page.
/// Remember to add the [[interaction_buttons]] as an action row! /// Remember to add the [[interaction_buttons]] as an action row!
async fn render( async fn render(&mut self, page: u8, btns: Vec<CreateActionRow>)
&mut self, -> Result<Option<CreateReply>>;
page: u8,
ctx: &Context,
m: &Message,
btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>>;
// /// The [[CreateActionRow]] for pagination. // /// The [[CreateActionRow]] for pagination.
// fn pagination_row(&self) -> CreateActionRow { // fn pagination_row(&self) -> CreateActionRow {
@ -54,11 +95,11 @@ pub trait Paginate: Send + Sized {
async fn handle_reaction( async fn handle_reaction(
&mut self, &mut self,
page: u8, page: u8,
ctx: &Context, _ctx: &Context,
message: &mut Message, message: &mut impl CanEdit,
reaction: &str, reaction: &str,
) -> Result<Option<u8>> { ) -> Result<Option<u8>> {
handle_pagination_reaction(page, self, ctx, message, reaction) handle_pagination_reaction(page, self, message, reaction)
.await .await
.map(Some) .map(Some)
} }
@ -82,25 +123,19 @@ pub trait Paginate: Send + Sized {
} }
} }
pub async fn do_render( pub async fn do_render(p: &mut impl Paginate, page: u8, m: &mut impl CanEdit) -> Result<bool> {
p: &mut impl Paginate,
page: u8,
ctx: &Context,
m: &mut Message,
) -> Result<bool> {
let btns = vec![CreateActionRow::Buttons(p.interaction_buttons())]; let btns = vec![CreateActionRow::Buttons(p.interaction_buttons())];
do_render_with_btns(p, page, ctx, m, btns).await do_render_with_btns(p, page, m, btns).await
} }
async fn do_render_with_btns( async fn do_render_with_btns(
p: &mut impl Paginate, p: &mut impl Paginate,
page: u8, page: u8,
ctx: &Context, m: &mut impl CanEdit,
m: &mut Message,
btns: Vec<CreateActionRow>, btns: Vec<CreateActionRow>,
) -> Result<bool> { ) -> Result<bool> {
if let Some(edit) = p.render(page, ctx, m, btns).await? { if let Some(edit) = p.render(page, btns).await? {
m.edit(ctx, edit).await?; m.apply_edit(edit).await?;
Ok(true) Ok(true)
} else { } else {
Ok(false) Ok(false)
@ -108,29 +143,24 @@ async fn do_render_with_btns(
} }
pub fn paginate_from_fn( pub fn paginate_from_fn(
pager: impl for<'m> FnMut( pager: impl FnMut(
u8, u8,
&'m Context,
&'m Message,
Vec<CreateActionRow>, Vec<CreateActionRow>,
) -> std::pin::Pin< )
Box<dyn Future<Output = Result<Option<EditMessage>>> + Send + 'm>, -> std::pin::Pin<Box<dyn Future<Output = Result<Option<CreateReply>>> + Send>>
> + Send, + Send,
) -> impl Paginate { ) -> impl Paginate {
pager pager
} }
pub fn default_buttons(p: &impl Paginate) -> Vec<CreateButton> { pub fn default_buttons(p: &impl Paginate) -> Vec<CreateButton> {
let mut btns = vec![ let mut btns = vec![
CreateButton::new(PREV).emoji(ReactionType::try_from(ARROW_LEFT).unwrap()), CreateButton::new(PREV).label("<"),
CreateButton::new(NEXT).emoji(ReactionType::try_from(ARROW_RIGHT).unwrap()), CreateButton::new(NEXT).label(">"),
]; ];
if p.len().is_some_and(|v| v > 5) { if p.len().is_some_and(|v| v > 5) {
btns.insert( btns.insert(0, CreateButton::new(FAST_PREV).label("<<"));
0, btns.push(CreateButton::new(FAST_NEXT).label(">>"))
CreateButton::new(FAST_PREV).emoji(ReactionType::try_from(REWIND).unwrap()),
);
btns.push(CreateButton::new(FAST_NEXT).emoji(ReactionType::try_from(FAST_FORWARD).unwrap()))
} }
btns btns
} }
@ -145,21 +175,19 @@ impl<Inner: Paginate> Paginate for WithPageCount<Inner> {
async fn render( async fn render(
&mut self, &mut self,
page: u8, page: u8,
ctx: &Context,
m: &Message,
btns: Vec<CreateActionRow>, btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>> { ) -> Result<Option<CreateReply>> {
if page as usize >= self.page_count { if page as usize >= self.page_count {
return Ok(None); return Ok(None);
} }
self.inner.render(page, ctx, m, btns).await self.inner.render(page, btns).await
} }
async fn handle_reaction( async fn handle_reaction(
&mut self, &mut self,
page: u8, page: u8,
ctx: &Context, ctx: &Context,
message: &mut Message, message: &mut impl CanEdit,
reaction: &str, reaction: &str,
) -> Result<Option<u8>> { ) -> Result<Option<u8>> {
self.inner self.inner
@ -179,23 +207,19 @@ impl<Inner: Paginate> Paginate for WithPageCount<Inner> {
#[async_trait::async_trait] #[async_trait::async_trait]
impl<T> Paginate for T impl<T> Paginate for T
where where
T: for<'m> FnMut( T: FnMut(
u8, u8,
&'m Context,
&'m Message,
Vec<CreateActionRow>, Vec<CreateActionRow>,
) -> std::pin::Pin< )
Box<dyn Future<Output = Result<Option<EditMessage>>> + Send + 'm>, -> std::pin::Pin<Box<dyn Future<Output = Result<Option<CreateReply>>> + Send>>
> + Send, + Send,
{ {
async fn render( async fn render(
&mut self, &mut self,
page: u8, page: u8,
ctx: &Context,
m: &Message,
btns: Vec<CreateActionRow>, btns: Vec<CreateActionRow>,
) -> Result<Option<EditMessage>> { ) -> Result<Option<CreateReply>> {
self(page, ctx, m, btns).await self(page, btns).await
} }
} }
@ -210,6 +234,7 @@ pub async fn paginate_reply(
let message = reply_to let message = reply_to
.reply(&ctx, "Youmu is loading the first page...") .reply(&ctx, "Youmu is loading the first page...")
.await?; .await?;
let message = (message, ctx);
paginate_with_first_message(pager, ctx, message, timeout).await paginate_with_first_message(pager, ctx, message, timeout).await
} }
@ -227,6 +252,7 @@ pub async fn paginate(
CreateMessage::new().content("Youmu is loading the first page..."), CreateMessage::new().content("Youmu is loading the first page..."),
) )
.await?; .await?;
let message = (message, ctx);
paginate_with_first_message(pager, ctx, message, timeout).await paginate_with_first_message(pager, ctx, message, timeout).await
} }
@ -234,13 +260,14 @@ pub async fn paginate(
pub async fn paginate_with_first_message( pub async fn paginate_with_first_message(
mut pager: impl Paginate, mut pager: impl Paginate,
ctx: &Context, ctx: &Context,
mut message: Message, mut message: impl CanEdit,
timeout: std::time::Duration, timeout: std::time::Duration,
) -> Result<()> { ) -> Result<()> {
let msg_id = message.get_message().await?.id;
let (send, recv) = flume::unbounded::<String>(); let (send, recv) = flume::unbounded::<String>();
Paginator::push(ctx, &message, send).await?; Paginator::push(ctx, msg_id, send).await?;
do_render(&mut pager, 0, ctx, &mut message).await?; do_render(&mut pager, 0, &mut message).await?;
// Just quit if there is only one page // Just quit if there is only one page
if pager.len().filter(|&v| v == 1).is_some() { if pager.len().filter(|&v| v == 1).is_some() {
return Ok(()); return Ok(());
@ -266,10 +293,10 @@ pub async fn paginate_with_first_message(
}; };
// Render one last time with no buttons // Render one last time with no buttons
do_render_with_btns(&mut pager, page, ctx, &mut message, vec![]) do_render_with_btns(&mut pager, page, &mut message, vec![])
.await .await
.pls_ok(); .pls_ok();
Paginator::pop(ctx, &message).await?; Paginator::pop(ctx, msg_id).await?;
res res
} }
@ -278,8 +305,7 @@ pub async fn paginate_with_first_message(
pub async fn handle_pagination_reaction( pub async fn handle_pagination_reaction(
page: u8, page: u8,
pager: &mut impl Paginate, pager: &mut impl Paginate,
ctx: &Context, message: &mut impl CanEdit,
message: &mut Message,
reaction: &str, reaction: &str,
) -> Result<u8> { ) -> Result<u8> {
let pages = pager.len(); let pages = pager.len();
@ -299,7 +325,7 @@ pub async fn handle_pagination_reaction(
FAST_NEXT => (pages.unwrap() as u8 - 1).min(page + fast), FAST_NEXT => (pages.unwrap() as u8 - 1).min(page + fast),
_ => return Ok(page), _ => return Ok(page),
}; };
Ok(if do_render(pager, new_page, ctx, message).await? { Ok(if do_render(pager, new_page, message).await? {
new_page new_page
} else { } else {
page page
@ -318,25 +344,25 @@ impl Paginator {
channels: Arc::new(dashmap::DashMap::new()), channels: Arc::new(dashmap::DashMap::new()),
} }
} }
async fn push(ctx: &Context, msg: &Message, channel: flume::Sender<String>) -> Result<()> { async fn push(ctx: &Context, msg: MessageId, channel: flume::Sender<String>) -> Result<()> {
ctx.data ctx.data
.write() .read()
.await .await
.get_mut::<Paginator>() .get::<Paginator>()
.unwrap() .unwrap()
.channels .channels
.insert(msg.id, channel); .insert(msg, channel);
Ok(()) Ok(())
} }
async fn pop(ctx: &Context, msg: &Message) -> Result<()> { async fn pop(ctx: &Context, msg: MessageId) -> Result<()> {
ctx.data ctx.data
.write() .read()
.await .await
.get_mut::<Paginator>() .get::<Paginator>()
.unwrap() .unwrap()
.channels .channels
.remove(&msg.id); .remove(&msg);
Ok(()) Ok(())
} }
} }