From a35563801dbf74c6f221aac3e70146572e3fe14e Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Thu, 20 Feb 2025 22:54:38 +0100 Subject: [PATCH] pagination: make editing generic over a CanEdit interface --- youmubot-cf/src/lib.rs | 14 +- youmubot-core/src/community/roles.rs | 7 +- youmubot-core/src/fun/images.rs | 5 +- youmubot-osu/src/discord/commands.rs | 28 ++-- youmubot-osu/src/discord/display.rs | 116 ++++++++-------- youmubot-osu/src/discord/hook.rs | 14 +- youmubot-osu/src/discord/interaction.rs | 54 ++++---- youmubot-osu/src/discord/mod.rs | 10 +- youmubot-osu/src/discord/server_rank.rs | 19 +-- youmubot-prelude/src/lib.rs | 3 +- youmubot-prelude/src/pagination.rs | 170 ++++++++++++++---------- 11 files changed, 233 insertions(+), 207 deletions(-) diff --git a/youmubot-cf/src/lib.rs b/youmubot-cf/src/lib.rs index d419a56..08f3f54 100644 --- a/youmubot-cf/src/lib.rs +++ b/youmubot-cf/src/lib.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; use codeforces::Contest; use pagination::paginate_from_fn; use serenity::{ - builder::{CreateMessage, EditMessage}, + builder::CreateMessage, framework::standard::{ macros::{command, group}, 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(); paginate_reply( - paginate_from_fn(move |page, _, _, btns| { + paginate_from_fn(move |page, btns| { use Align::*; let ranks = ranks.clone(); Box::pin(async move { @@ -222,7 +222,9 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult { )) .build(); - Ok(Some(EditMessage::new().content(content).components(btns))) + Ok(Some( + CreateReply::default().content(content).components(btns), + )) }) }) .with_page_count(total_pages), @@ -317,7 +319,7 @@ pub(crate) async fn contest_rank_table( let ranks = Arc::new(ranks); paginate_reply( - paginate_from_fn(move |page, _, _, btns| { + paginate_from_fn(move |page, btns| { let contest = contest.clone(); let problems = problems.clone(); let ranks = ranks.clone(); @@ -388,7 +390,9 @@ pub(crate) async fn contest_rank_table( .push_line(format!("Page **{}/{}**", page + 1, total_pages)) .build(); - Ok(Some(EditMessage::new().content(content).components(btns))) + Ok(Some( + CreateReply::default().content(content).components(btns), + )) }) }) .with_page_count(total_pages), diff --git a/youmubot-core/src/community/roles.rs b/youmubot-core/src/community/roles.rs index dfb96ab..eaaf9d8 100644 --- a/youmubot-core/src/community/roles.rs +++ b/youmubot-core/src/community/roles.rs @@ -1,5 +1,4 @@ use serenity::{ - builder::EditMessage, framework::standard::{macros::command, Args, CommandResult}, model::{ 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; paginate_reply( - paginate_from_fn(|page, _, _, btns| { + paginate_from_fn(|page, btns| { let roles = roles.clone(); Box::pin(async move { 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)) .build(); - Ok(Some(EditMessage::new().content(content).components(btns))) + Ok(Some( + CreateReply::default().content(content).components(btns), + )) }) }) .with_page_count(pages), diff --git a/youmubot-core/src/fun/images.rs b/youmubot-core/src/fun/images.rs index 48c0fb3..6f125dc 100644 --- a/youmubot-core/src/fun/images.rs +++ b/youmubot-core/src/fun/images.rs @@ -1,5 +1,4 @@ use serde::Deserialize; -use serenity::builder::EditMessage; use serenity::framework::standard::CommandError as Error; use serenity::{ framework::standard::{ @@ -66,7 +65,7 @@ async fn message_command( } let images = std::sync::Arc::new(images); paginate_reply( - paginate_from_fn(|page, _, _, btns| { + paginate_from_fn(|page, btns| { let images = images.clone(); Box::pin(async move { let page = page as usize; @@ -74,7 +73,7 @@ async fn message_command( Ok(None) } else { Ok(Some( - EditMessage::new() + CreateReply::default() .content(format!( "[🖼️ **{}/{}**] Here's the image you requested!\n\n{}", page + 1, diff --git a/youmubot-osu/src/discord/commands.rs b/youmubot-osu/src/discord/commands.rs index 1bad469..ac6f150 100644 --- a/youmubot-osu/src/discord/commands.rs +++ b/youmubot-osu/src/discord/commands.rs @@ -294,11 +294,14 @@ async fn handle_listing( 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( 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( args.user.mention(), display )) - .await? - .into_message() .await?; let style = style.unwrap_or(if scores.len() <= 5 { @@ -481,7 +480,12 @@ async fn check( }); 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( .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?; } diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index 1d5b381..c3134ae 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -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, ctx: &Context, guild_id: Option, - 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, ctx: &Context, guild_id: Option, - mut on: Message, + mut on: impl CanEdit, ) -> Result<()> { + let env = ctx.data.read().await.get::().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, guild_id: Option, + 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, - ) -> Result> { - let env = ctx.data.read().await.get::().unwrap().clone(); + ) -> Result> { + 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, 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::().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, } @@ -201,11 +206,9 @@ mod scores { async fn render( &mut self, page: u8, - ctx: &Context, - _: &Message, btns: Vec, - ) -> Result> { - let env = ctx.data.read().await.get::().unwrap().clone(); + ) -> Result> { + 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 { @@ -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, mode: Option, mods: Option, guild_id: Option, - target: Message, + target: impl CanEdit, ) -> Result { assert!(!beatmapset.is_empty(), "Beatmapset should not be empty"); @@ -381,6 +386,8 @@ mod beatmapset { }); let p = Paginate { + env: ctx.data.read().await.get::().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, infos: Vec>, mode: Option, @@ -411,15 +418,9 @@ mod beatmapset { } impl Paginate { - async fn get_beatmap_info( - &self, - ctx: &Context, - b: &Beatmap, - mods: &Mods, - ) -> Result { - let env = ctx.data.read().await.get::().unwrap().clone(); - - env.oppai + async fn get_beatmap_info(&self, b: &Beatmap, mods: &Mods) -> Result { + 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, - ) -> Result> { + ) -> Result> { 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::().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> { // 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) } diff --git a/youmubot-osu/src/discord/hook.rs b/youmubot-osu/src/discord/hook.rs index 0c4145b..a8ad8d5 100644 --- a/youmubot-osu/src/discord/hook.rs +++ b/youmubot-osu/src/discord/hook.rs @@ -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(); diff --git a/youmubot-osu/src/discord/interaction.rs b/youmubot-osu/src/discord/interaction.rs index 2f11565..115c9b0 100644 --- a/youmubot-osu/src/discord/interaction.rs +++ b/youmubot-osu/src/discord/interaction.rs @@ -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 [`{}`]() on {}!", - user.username, - user.id, - embed.mention() - )), - ) - .await?; + comp.create_followup( + &ctx, + CreateInteractionResponseFollowup::new().content(format!( + "Here are the scores by [`{}`]() 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)]), ) diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 50c98a4..d60e139 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -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?; } } diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index ce4e1d6..e6b7af1 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -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(""), )) .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?; diff --git a/youmubot-prelude/src/lib.rs b/youmubot-prelude/src/lib.rs index 76f03bf..c536b2a 100644 --- a/youmubot-prelude/src/lib.rs +++ b/youmubot-prelude/src/lib.rs @@ -18,7 +18,8 @@ pub use debugging_ok::OkPrint; pub use flags::Flags; pub use hook::Hook; 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 args; diff --git a/youmubot-prelude/src/pagination.rs b/youmubot-prelude/src/pagination.rs index 397b2c6..e8356b2 100644 --- a/youmubot-prelude/src/pagination.rs +++ b/youmubot-prelude/src/pagination.rs @@ -1,9 +1,10 @@ -use crate::{Context, OkPrint, Result}; +use crate::{CmdContext, Context, OkPrint, Result}; use futures_util::future::Future; +use poise::{CreateReply, ReplyHandle}; use serenity::{ all::{ - CreateActionRow, CreateButton, CreateInteractionResponse, EditMessage, Interaction, - MessageId, + ComponentInteraction, CreateActionRow, CreateButton, CreateInteractionResponse, + EditInteractionResponse, EditMessage, Interaction, MessageId, }, builder::CreateMessage, model::{ @@ -15,28 +16,68 @@ use serenity::{ use std::{convert::TryFrom, sync::Arc}; use tokio::time as tokio_time; -const ARROW_RIGHT: &str = "➡️"; -const ARROW_LEFT: &str = "⬅️"; -const REWIND: &str = "⏪"; -const FAST_FORWARD: &str = "⏩"; +// const ARROW_RIGHT: &str = "➡️"; +// const ARROW_LEFT: &str = "⬅️"; +// const REWIND: &str = "⏪"; +// const FAST_FORWARD: &str = "⏩"; const NEXT: &str = "youmubot_pagination_next"; const PREV: &str = "youmubot_pagination_prev"; const FAST_NEXT: &str = "youmubot_pagination_fast_next"; const FAST_PREV: &str = "youmubot_pagination_fast_prev"; +pub trait CanEdit: Send { + fn get_message(&self) -> impl Future> + Send; + fn apply_edit(&mut self, edit: CreateReply) -> impl Future> + Send; +} + +impl<'a> CanEdit for (Message, &'a Context) { + async fn get_message(&self) -> Result { + 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 { + 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 { + 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. #[async_trait::async_trait] pub trait Paginate: Send + Sized { /// Render the given page. /// Remember to add the [[interaction_buttons]] as an action row! - async fn render( - &mut self, - page: u8, - ctx: &Context, - m: &Message, - btns: Vec, - ) -> Result>; + async fn render(&mut self, page: u8, btns: Vec) + -> Result>; // /// The [[CreateActionRow]] for pagination. // fn pagination_row(&self) -> CreateActionRow { @@ -54,11 +95,11 @@ pub trait Paginate: Send + Sized { async fn handle_reaction( &mut self, page: u8, - ctx: &Context, - message: &mut Message, + _ctx: &Context, + message: &mut impl CanEdit, reaction: &str, ) -> Result> { - handle_pagination_reaction(page, self, ctx, message, reaction) + handle_pagination_reaction(page, self, message, reaction) .await .map(Some) } @@ -82,25 +123,19 @@ pub trait Paginate: Send + Sized { } } -pub async fn do_render( - p: &mut impl Paginate, - page: u8, - ctx: &Context, - m: &mut Message, -) -> Result { +pub async fn do_render(p: &mut impl Paginate, page: u8, m: &mut impl CanEdit) -> Result { 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( p: &mut impl Paginate, page: u8, - ctx: &Context, - m: &mut Message, + m: &mut impl CanEdit, btns: Vec, ) -> Result { - if let Some(edit) = p.render(page, ctx, m, btns).await? { - m.edit(ctx, edit).await?; + if let Some(edit) = p.render(page, btns).await? { + m.apply_edit(edit).await?; Ok(true) } else { Ok(false) @@ -108,29 +143,24 @@ async fn do_render_with_btns( } pub fn paginate_from_fn( - pager: impl for<'m> FnMut( + pager: impl FnMut( u8, - &'m Context, - &'m Message, Vec, - ) -> std::pin::Pin< - Box>> + Send + 'm>, - > + Send, + ) + -> std::pin::Pin>> + Send>> + + Send, ) -> impl Paginate { pager } pub fn default_buttons(p: &impl Paginate) -> Vec { let mut btns = vec![ - CreateButton::new(PREV).emoji(ReactionType::try_from(ARROW_LEFT).unwrap()), - CreateButton::new(NEXT).emoji(ReactionType::try_from(ARROW_RIGHT).unwrap()), + CreateButton::new(PREV).label("<"), + CreateButton::new(NEXT).label(">"), ]; if p.len().is_some_and(|v| v > 5) { - btns.insert( - 0, - CreateButton::new(FAST_PREV).emoji(ReactionType::try_from(REWIND).unwrap()), - ); - btns.push(CreateButton::new(FAST_NEXT).emoji(ReactionType::try_from(FAST_FORWARD).unwrap())) + btns.insert(0, CreateButton::new(FAST_PREV).label("<<")); + btns.push(CreateButton::new(FAST_NEXT).label(">>")) } btns } @@ -145,21 +175,19 @@ impl Paginate for WithPageCount { async fn render( &mut self, page: u8, - ctx: &Context, - m: &Message, btns: Vec, - ) -> Result> { + ) -> Result> { if page as usize >= self.page_count { return Ok(None); } - self.inner.render(page, ctx, m, btns).await + self.inner.render(page, btns).await } async fn handle_reaction( &mut self, page: u8, ctx: &Context, - message: &mut Message, + message: &mut impl CanEdit, reaction: &str, ) -> Result> { self.inner @@ -179,23 +207,19 @@ impl Paginate for WithPageCount { #[async_trait::async_trait] impl Paginate for T where - T: for<'m> FnMut( + T: FnMut( u8, - &'m Context, - &'m Message, Vec, - ) -> std::pin::Pin< - Box>> + Send + 'm>, - > + Send, + ) + -> std::pin::Pin>> + Send>> + + Send, { async fn render( &mut self, page: u8, - ctx: &Context, - m: &Message, btns: Vec, - ) -> Result> { - self(page, ctx, m, btns).await + ) -> Result> { + self(page, btns).await } } @@ -210,6 +234,7 @@ pub async fn paginate_reply( let message = reply_to .reply(&ctx, "Youmu is loading the first page...") .await?; + let message = (message, ctx); 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..."), ) .await?; + let message = (message, ctx); paginate_with_first_message(pager, ctx, message, timeout).await } @@ -234,13 +260,14 @@ pub async fn paginate( pub async fn paginate_with_first_message( mut pager: impl Paginate, ctx: &Context, - mut message: Message, + mut message: impl CanEdit, timeout: std::time::Duration, ) -> Result<()> { + let msg_id = message.get_message().await?.id; let (send, recv) = flume::unbounded::(); - 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 if pager.len().filter(|&v| v == 1).is_some() { return Ok(()); @@ -266,10 +293,10 @@ pub async fn paginate_with_first_message( }; // 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 .pls_ok(); - Paginator::pop(ctx, &message).await?; + Paginator::pop(ctx, msg_id).await?; res } @@ -278,8 +305,7 @@ pub async fn paginate_with_first_message( pub async fn handle_pagination_reaction( page: u8, pager: &mut impl Paginate, - ctx: &Context, - message: &mut Message, + message: &mut impl CanEdit, reaction: &str, ) -> Result { let pages = pager.len(); @@ -299,7 +325,7 @@ pub async fn handle_pagination_reaction( FAST_NEXT => (pages.unwrap() as u8 - 1).min(page + fast), _ => return Ok(page), }; - Ok(if do_render(pager, new_page, ctx, message).await? { + Ok(if do_render(pager, new_page, message).await? { new_page } else { page @@ -318,25 +344,25 @@ impl Paginator { channels: Arc::new(dashmap::DashMap::new()), } } - async fn push(ctx: &Context, msg: &Message, channel: flume::Sender) -> Result<()> { + async fn push(ctx: &Context, msg: MessageId, channel: flume::Sender) -> Result<()> { ctx.data - .write() + .read() .await - .get_mut::() + .get::() .unwrap() .channels - .insert(msg.id, channel); + .insert(msg, channel); Ok(()) } - async fn pop(ctx: &Context, msg: &Message) -> Result<()> { + async fn pop(ctx: &Context, msg: MessageId) -> Result<()> { ctx.data - .write() + .read() .await - .get_mut::() + .get::() .unwrap() .channels - .remove(&msg.id); + .remove(&msg); Ok(()) } }