From 189cb195a425ab9b747c9139049792e00f5489c8 Mon Sep 17 00:00:00 2001 From: jude Date: Tue, 13 Sep 2022 12:37:50 +0100 Subject: [PATCH] Add paging for list commands --- Cargo.lock | 1 + Cargo.toml | 1 + src/cmds/info.rs | 4 +- src/cmds/search.rs | 250 +++++++++++++++++++++++++++++++++--------- src/consts.rs | 5 +- src/event_handlers.rs | 36 +++--- src/models/sound.rs | 119 ++++++++++++++++---- 7 files changed, 327 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f9bb86..b03a1df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1933,6 +1933,7 @@ dependencies = [ "poise", "regex", "reqwest", + "serde", "serde_json", "songbird", "sqlx", diff --git a/Cargo.toml b/Cargo.toml index 08d5f3a..33dc1ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ regex = "1.4" log = "0.4" serde_json = "1.0" dashmap = "5.3" +serde = "1.0" [patch."https://github.com/serenity-rs/serenity"] serenity = { version = "0.11.5" } diff --git a/src/cmds/info.rs b/src/cmds/info.rs index 5461173..daea9d6 100644 --- a/src/cmds/info.rs +++ b/src/cmds/info.rs @@ -4,7 +4,7 @@ use crate::{consts::THEME_COLOR, Context, Error}; #[poise::command(slash_command)] pub async fn help(ctx: Context<'_>) -> Result<(), Error> { ctx.send(|m| { - m.embed(|e| { + m.ephemeral(true).embed(|e| { e.title("Help") .color(THEME_COLOR) .footer(|f| { @@ -59,7 +59,7 @@ __Advanced Commands__ pub async fn info(ctx: Context<'_>) -> Result<(), Error> { let current_user = ctx.discord().cache.current_user(); - ctx.send(|m| m + ctx.send(|m| m.ephemeral(true) .embed(|e| e .title("Info") .color(THEME_COLOR) diff --git a/src/cmds/search.rs b/src/cmds/search.rs index a352b14..2c8cc80 100644 --- a/src/cmds/search.rs +++ b/src/cmds/search.rs @@ -1,8 +1,19 @@ -use poise::{serenity::constants::MESSAGE_CODE_LIMIT, CreateReply}; +use poise::{ + serenity_prelude, + serenity_prelude::{ + application::component::ButtonStyle, + constants::MESSAGE_CODE_LIMIT, + interaction::{message_component::MessageComponentInteraction, InteractionResponseType}, + CreateActionRow, CreateEmbed, GuildId, UserId, + }, + CreateReply, +}; +use serde::{Deserialize, Serialize}; use crate::{ + consts::THEME_COLOR, models::sound::{Sound, SoundCtx}, - Context, Error, + Context, Data, Error, }; fn format_search_results<'a>(search_results: Vec) -> CreateReply<'a> { @@ -32,37 +43,31 @@ pub async fn list_sounds(_ctx: Context<'_>) -> Result<(), Error> { Ok(()) } +#[derive(Serialize, Deserialize, Clone, Copy)] +enum ListContext { + User = 0, + Guild = 1, +} + +impl ListContext { + pub fn title(&self) -> &'static str { + match self { + ListContext::User => "Your sounds", + ListContext::Guild => "Server sounds", + } + } +} + /// Show the sounds uploaded to this server #[poise::command(slash_command, rename = "server", guild_only = true)] pub async fn list_guild_sounds(ctx: Context<'_>) -> Result<(), Error> { - let sounds; - let mut message_buffer; + let pager = SoundPager { + nonce: 0, + page: 0, + context: ListContext::Guild, + }; - sounds = ctx.data().guild_sounds(ctx.guild_id().unwrap()).await?; - - message_buffer = "Sounds on this server: ".to_string(); - - // todo change this to iterator - for sound in sounds { - message_buffer.push_str( - format!( - "**{}** ({}), ", - sound.name, - if sound.public { "🔓" } else { "🔒" } - ) - .as_str(), - ); - - if message_buffer.len() > 2000 { - ctx.say(message_buffer).await?; - - message_buffer = "".to_string(); - } - } - - if message_buffer.len() > 0 { - ctx.say(message_buffer).await?; - } + pager.reply(ctx).await?; Ok(()) } @@ -70,36 +75,181 @@ pub async fn list_guild_sounds(ctx: Context<'_>) -> Result<(), Error> { /// Show all sounds you have uploaded #[poise::command(slash_command, rename = "user", guild_only = true)] pub async fn list_user_sounds(ctx: Context<'_>) -> Result<(), Error> { - let sounds; - let mut message_buffer; + let pager = SoundPager { + nonce: 0, + page: 0, + context: ListContext::User, + }; - sounds = ctx.data().user_sounds(ctx.author().id).await?; + pager.reply(ctx).await?; - message_buffer = "Sounds on this server: ".to_string(); + Ok(()) +} - // todo change this to iterator - for sound in sounds { - message_buffer.push_str( - format!( - "**{}** ({}), ", - sound.name, - if sound.public { "🔓" } else { "🔒" } - ) - .as_str(), - ); +#[derive(Serialize, Deserialize)] +pub struct SoundPager { + nonce: u64, + page: u64, + context: ListContext, +} - if message_buffer.len() > 2000 { - ctx.say(message_buffer).await?; - - message_buffer = "".to_string(); +impl SoundPager { + async fn get_page( + &self, + data: &Data, + user_id: UserId, + guild_id: GuildId, + ) -> Result, sqlx::Error> { + match self.context { + ListContext::User => data.user_sounds(user_id, Some(self.page)).await, + ListContext::Guild => data.guild_sounds(guild_id, Some(self.page)).await, } } - if message_buffer.len() > 0 { - ctx.say(message_buffer).await?; + fn create_action_row(&self, max_page: u64) -> CreateActionRow { + let mut row = CreateActionRow::default(); + + row.create_button(|b| { + b.custom_id( + serde_json::to_string(&SoundPager { + nonce: 0, + page: 0, + context: self.context, + }) + .unwrap(), + ) + .style(ButtonStyle::Primary) + .label("⏪") + .disabled(self.page == 0) + }) + .create_button(|b| { + b.custom_id( + serde_json::to_string(&SoundPager { + nonce: 1, + page: self.page.saturating_sub(1), + context: self.context, + }) + .unwrap(), + ) + .style(ButtonStyle::Secondary) + .label("◀️") + .disabled(self.page == 0) + }) + .create_button(|b| { + b.custom_id("pid") + .style(ButtonStyle::Success) + .label(format!("Page {}", self.page + 1)) + .disabled(true) + }) + .create_button(|b| { + b.custom_id( + serde_json::to_string(&SoundPager { + nonce: 2, + page: self.page.saturating_add(1), + context: self.context, + }) + .unwrap(), + ) + .style(ButtonStyle::Secondary) + .label("▶️") + .disabled(self.page == max_page) + }) + .create_button(|b| { + b.custom_id( + serde_json::to_string(&SoundPager { + nonce: 3, + page: max_page, + context: self.context, + }) + .unwrap(), + ) + .style(ButtonStyle::Primary) + .label("⏩") + .disabled(self.page == max_page) + }); + + row } - Ok(()) + fn embed(&self, sounds: &[Sound], count: u64) -> CreateEmbed { + let mut embed = CreateEmbed::default(); + + embed + .color(THEME_COLOR) + .title(self.context.title()) + .description(format!("**{}** sounds:", count)) + .fields(sounds.iter().map(|s| { + ( + s.name.as_str(), + if s.public { "*Public*" } else { "*Private*" }, + true, + ) + })); + + embed + } + + pub async fn handle_interaction( + ctx: &serenity_prelude::Context, + data: &Data, + interaction: &MessageComponentInteraction, + ) -> Result<(), Error> { + let user_id = interaction.user.id; + let guild_id = interaction.guild_id.unwrap(); + + match serde_json::from_str::(&interaction.data.custom_id) { + Ok(pager) => { + let sounds = pager.get_page(data, user_id, guild_id).await?; + let count = match pager.context { + ListContext::User => data.count_user_sounds(user_id).await?, + ListContext::Guild => data.count_guild_sounds(guild_id).await?, + }; + + interaction + .create_interaction_response(&ctx, |r| { + r.kind(InteractionResponseType::UpdateMessage) + .interaction_response_data(|d| { + d.ephemeral(true) + .add_embed(pager.embed(&sounds, count)) + .components(|c| { + c.add_action_row(pager.create_action_row(count / 25)) + }) + }) + }) + .await?; + + Ok(()) + } + + Err(_) => Ok(()), + } + } + + async fn reply(&self, ctx: Context<'_>) -> Result<(), Error> { + let sounds = self + .get_page(ctx.data(), ctx.author().id, ctx.guild_id().unwrap()) + .await?; + let count = match self.context { + ListContext::User => ctx.data().count_user_sounds(ctx.author().id).await?, + ListContext::Guild => { + ctx.data() + .count_guild_sounds(ctx.guild_id().unwrap()) + .await? + } + }; + + ctx.send(|r| { + r.ephemeral(true) + .embed(|e| { + *e = self.embed(&sounds, count); + e + }) + .components(|c| c.add_action_row(self.create_action_row(count / 25))) + }) + .await?; + + Ok(()) + } } /// Search for sounds diff --git a/src/consts.rs b/src/consts.rs index c98f149..39a9352 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -7,7 +7,10 @@ lazy_static! { .unwrap_or_else(|_| "2097152".to_string()) .parse::() .unwrap(); - pub static ref MAX_SOUNDS: u32 = env::var("MAX_SOUNDS").unwrap().parse::().unwrap(); + pub static ref MAX_SOUNDS: u32 = env::var("MAX_SOUNDS") + .unwrap_or_else(|_| "8".to_string()) + .parse::() + .unwrap(); pub static ref PATREON_GUILD: u64 = env::var("PATREON_GUILD").unwrap().parse::().unwrap(); pub static ref PATREON_ROLE: u64 = env::var("PATREON_ROLE").unwrap().parse::().unwrap(); } diff --git a/src/event_handlers.rs b/src/event_handlers.rs index a301ca0..4622e4b 100644 --- a/src/event_handlers.rs +++ b/src/event_handlers.rs @@ -13,6 +13,7 @@ use poise::{ }; use crate::{ + cmds::search::SoundPager, models::{guild_data::CtxGuildData, join_sound::JoinSoundCtx, sound::Sound}, utils::{join_channel, play_audio, play_from_query}, Data, Error, @@ -126,23 +127,26 @@ SELECT name, id, public, server_id, uploader_id } poise::Event::InteractionCreate { interaction } => match interaction { Interaction::MessageComponent(component) => { - if component.guild_id.is_some() { - component - .create_interaction_response(ctx, |r| { - r.kind(InteractionResponseType::DeferredUpdateMessage) - }) - .await - .unwrap(); + if let Some(guild_id) = component.guild_id { + if let Ok(()) = SoundPager::handle_interaction(ctx, &data, component).await { + } else { + component + .create_interaction_response(ctx, |r| { + r.kind(InteractionResponseType::DeferredUpdateMessage) + }) + .await + .unwrap(); - play_from_query( - &ctx, - &data, - component.guild_id.unwrap().to_guild_cached(&ctx).unwrap(), - component.user.id, - &component.data.custom_id, - false, - ) - .await; + play_from_query( + &ctx, + &data, + guild_id.to_guild_cached(&ctx).unwrap(), + component.user.id, + &component.data.custom_id, + false, + ) + .await; + } } } _ => {} diff --git a/src/models/sound.rs b/src/models/sound.rs index 7f57c56..752d52f 100644 --- a/src/models/sound.rs +++ b/src/models/sound.rs @@ -2,7 +2,7 @@ use std::{env, path::Path}; use poise::serenity::async_trait; use songbird::input::restartable::Restartable; -use sqlx::{Error, Executor}; +use sqlx::Executor; use tokio::{fs::File, io::AsyncWriteExt, process::Command}; use crate::{consts::UPLOAD_MAX_SIZE, error::ErrorTypes, Data, Database}; @@ -37,12 +37,21 @@ pub trait SoundCtx { user_id: U, guild_id: G, ) -> Result, sqlx::Error>; - async fn user_sounds + Send>(&self, user_id: U) - -> Result, sqlx::Error>; + async fn user_sounds + Send>( + &self, + user_id: U, + page: Option, + ) -> Result, sqlx::Error>; async fn guild_sounds + Send>( &self, guild_id: G, + page: Option, ) -> Result, sqlx::Error>; + async fn count_user_sounds + Send>(&self, user_id: U) -> Result; + async fn count_guild_sounds + Send>( + &self, + guild_id: G, + ) -> Result; } #[async_trait] @@ -149,7 +158,7 @@ SELECT name, id, public, server_id, uploader_id query: &str, user_id: U, guild_id: G, - ) -> Result, Error> { + ) -> Result, sqlx::Error> { let db_pool = self.database.clone(); sqlx::query_as_unchecked!( @@ -171,18 +180,41 @@ LIMIT 25 async fn user_sounds + Send>( &self, user_id: U, + page: Option, ) -> Result, sqlx::Error> { - let sounds = sqlx::query_as_unchecked!( - Sound, - " + let sounds = match page { + Some(page) => { + sqlx::query_as_unchecked!( + Sound, + " SELECT name, id, public, server_id, uploader_id FROM sounds WHERE uploader_id = ? + ORDER BY name + LIMIT ?, ? ", - user_id.into() - ) - .fetch_all(&self.database) - .await?; + user_id.into(), + page * 25, + (page + 1) * 25 + ) + .fetch_all(&self.database) + .await? + } + None => { + sqlx::query_as_unchecked!( + Sound, + " +SELECT name, id, public, server_id, uploader_id + FROM sounds + WHERE uploader_id = ? + ORDER BY name + ", + user_id.into() + ) + .fetch_all(&self.database) + .await? + } + }; Ok(sounds) } @@ -190,21 +222,68 @@ SELECT name, id, public, server_id, uploader_id async fn guild_sounds + Send>( &self, guild_id: G, + page: Option, ) -> Result, sqlx::Error> { - let sounds = sqlx::query_as_unchecked!( - Sound, - " + let sounds = match page { + Some(page) => { + sqlx::query_as_unchecked!( + Sound, + " SELECT name, id, public, server_id, uploader_id FROM sounds WHERE server_id = ? + ORDER BY name + LIMIT ?, ? ", - guild_id.into() - ) - .fetch_all(&self.database) - .await?; + guild_id.into(), + page * 25, + (page + 1) * 25 + ) + .fetch_all(&self.database) + .await? + } + + None => { + sqlx::query_as_unchecked!( + Sound, + " +SELECT name, id, public, server_id, uploader_id + FROM sounds + WHERE server_id = ? + ORDER BY name + ", + guild_id.into() + ) + .fetch_all(&self.database) + .await? + } + }; Ok(sounds) } + + async fn count_user_sounds + Send>(&self, user_id: U) -> Result { + Ok(sqlx::query!( + "SELECT COUNT(1) as count FROM sounds WHERE uploader_id = ?", + user_id.into() + ) + .fetch_one(&self.database) + .await? + .count as u64) + } + + async fn count_guild_sounds + Send>( + &self, + guild_id: G, + ) -> Result { + Ok(sqlx::query!( + "SELECT COUNT(1) as count FROM sounds WHERE server_id = ?", + guild_id.into() + ) + .fetch_one(&self.database) + .await? + .count as u64) + } } impl Sound { @@ -262,7 +341,7 @@ SELECT src pub async fn count_user_sounds>( user_id: U, db_pool: impl Executor<'_, Database = Database>, - ) -> Result { + ) -> Result { let user_id = user_id.into(); let c = sqlx::query!( @@ -284,7 +363,7 @@ SELECT COUNT(1) as count user_id: U, name: &String, db_pool: impl Executor<'_, Database = Database>, - ) -> Result { + ) -> Result { let user_id = user_id.into(); let c = sqlx::query!(