Add paging for list commands

This commit is contained in:
jude 2022-09-13 12:37:50 +01:00
parent a05d6f77db
commit 189cb195a4
7 changed files with 327 additions and 89 deletions

1
Cargo.lock generated
View File

@ -1933,6 +1933,7 @@ dependencies = [
"poise", "poise",
"regex", "regex",
"reqwest", "reqwest",
"serde",
"serde_json", "serde_json",
"songbird", "songbird",
"sqlx", "sqlx",

View File

@ -16,6 +16,7 @@ regex = "1.4"
log = "0.4" log = "0.4"
serde_json = "1.0" serde_json = "1.0"
dashmap = "5.3" dashmap = "5.3"
serde = "1.0"
[patch."https://github.com/serenity-rs/serenity"] [patch."https://github.com/serenity-rs/serenity"]
serenity = { version = "0.11.5" } serenity = { version = "0.11.5" }

View File

@ -4,7 +4,7 @@ use crate::{consts::THEME_COLOR, Context, Error};
#[poise::command(slash_command)] #[poise::command(slash_command)]
pub async fn help(ctx: Context<'_>) -> Result<(), Error> { pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(|m| { ctx.send(|m| {
m.embed(|e| { m.ephemeral(true).embed(|e| {
e.title("Help") e.title("Help")
.color(THEME_COLOR) .color(THEME_COLOR)
.footer(|f| { .footer(|f| {
@ -59,7 +59,7 @@ __Advanced Commands__
pub async fn info(ctx: Context<'_>) -> Result<(), Error> { pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
let current_user = ctx.discord().cache.current_user(); let current_user = ctx.discord().cache.current_user();
ctx.send(|m| m ctx.send(|m| m.ephemeral(true)
.embed(|e| e .embed(|e| e
.title("Info") .title("Info")
.color(THEME_COLOR) .color(THEME_COLOR)

View File

@ -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::{ use crate::{
consts::THEME_COLOR,
models::sound::{Sound, SoundCtx}, models::sound::{Sound, SoundCtx},
Context, Error, Context, Data, Error,
}; };
fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> { fn format_search_results<'a>(search_results: Vec<Sound>) -> CreateReply<'a> {
@ -32,37 +43,31 @@ pub async fn list_sounds(_ctx: Context<'_>) -> Result<(), Error> {
Ok(()) 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 /// Show the sounds uploaded to this server
#[poise::command(slash_command, rename = "server", guild_only = true)] #[poise::command(slash_command, rename = "server", guild_only = true)]
pub async fn list_guild_sounds(ctx: Context<'_>) -> Result<(), Error> { pub async fn list_guild_sounds(ctx: Context<'_>) -> Result<(), Error> {
let sounds; let pager = SoundPager {
let mut message_buffer; nonce: 0,
page: 0,
context: ListContext::Guild,
};
sounds = ctx.data().guild_sounds(ctx.guild_id().unwrap()).await?; pager.reply(ctx).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?;
}
Ok(()) Ok(())
} }
@ -70,38 +75,183 @@ pub async fn list_guild_sounds(ctx: Context<'_>) -> Result<(), Error> {
/// Show all sounds you have uploaded /// Show all sounds you have uploaded
#[poise::command(slash_command, rename = "user", guild_only = true)] #[poise::command(slash_command, rename = "user", guild_only = true)]
pub async fn list_user_sounds(ctx: Context<'_>) -> Result<(), Error> { pub async fn list_user_sounds(ctx: Context<'_>) -> Result<(), Error> {
let sounds; let pager = SoundPager {
let mut message_buffer; 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();
// 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?;
}
Ok(()) Ok(())
} }
#[derive(Serialize, Deserialize)]
pub struct SoundPager {
nonce: u64,
page: u64,
context: ListContext,
}
impl SoundPager {
async fn get_page(
&self,
data: &Data,
user_id: UserId,
guild_id: GuildId,
) -> Result<Vec<Sound>, 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,
}
}
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
}
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::<Self>(&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 /// Search for sounds
#[poise::command( #[poise::command(
slash_command, slash_command,

View File

@ -7,7 +7,10 @@ lazy_static! {
.unwrap_or_else(|_| "2097152".to_string()) .unwrap_or_else(|_| "2097152".to_string())
.parse::<u64>() .parse::<u64>()
.unwrap(); .unwrap();
pub static ref MAX_SOUNDS: u32 = env::var("MAX_SOUNDS").unwrap().parse::<u32>().unwrap(); pub static ref MAX_SOUNDS: u32 = env::var("MAX_SOUNDS")
.unwrap_or_else(|_| "8".to_string())
.parse::<u32>()
.unwrap();
pub static ref PATREON_GUILD: u64 = env::var("PATREON_GUILD").unwrap().parse::<u64>().unwrap(); pub static ref PATREON_GUILD: u64 = env::var("PATREON_GUILD").unwrap().parse::<u64>().unwrap();
pub static ref PATREON_ROLE: u64 = env::var("PATREON_ROLE").unwrap().parse::<u64>().unwrap(); pub static ref PATREON_ROLE: u64 = env::var("PATREON_ROLE").unwrap().parse::<u64>().unwrap();
} }

View File

@ -13,6 +13,7 @@ use poise::{
}; };
use crate::{ use crate::{
cmds::search::SoundPager,
models::{guild_data::CtxGuildData, join_sound::JoinSoundCtx, sound::Sound}, models::{guild_data::CtxGuildData, join_sound::JoinSoundCtx, sound::Sound},
utils::{join_channel, play_audio, play_from_query}, utils::{join_channel, play_audio, play_from_query},
Data, Error, Data, Error,
@ -126,7 +127,9 @@ SELECT name, id, public, server_id, uploader_id
} }
poise::Event::InteractionCreate { interaction } => match interaction { poise::Event::InteractionCreate { interaction } => match interaction {
Interaction::MessageComponent(component) => { Interaction::MessageComponent(component) => {
if component.guild_id.is_some() { if let Some(guild_id) = component.guild_id {
if let Ok(()) = SoundPager::handle_interaction(ctx, &data, component).await {
} else {
component component
.create_interaction_response(ctx, |r| { .create_interaction_response(ctx, |r| {
r.kind(InteractionResponseType::DeferredUpdateMessage) r.kind(InteractionResponseType::DeferredUpdateMessage)
@ -137,7 +140,7 @@ SELECT name, id, public, server_id, uploader_id
play_from_query( play_from_query(
&ctx, &ctx,
&data, &data,
component.guild_id.unwrap().to_guild_cached(&ctx).unwrap(), guild_id.to_guild_cached(&ctx).unwrap(),
component.user.id, component.user.id,
&component.data.custom_id, &component.data.custom_id,
false, false,
@ -145,6 +148,7 @@ SELECT name, id, public, server_id, uploader_id
.await; .await;
} }
} }
}
_ => {} _ => {}
}, },
_ => {} _ => {}

View File

@ -2,7 +2,7 @@ use std::{env, path::Path};
use poise::serenity::async_trait; use poise::serenity::async_trait;
use songbird::input::restartable::Restartable; use songbird::input::restartable::Restartable;
use sqlx::{Error, Executor}; use sqlx::Executor;
use tokio::{fs::File, io::AsyncWriteExt, process::Command}; use tokio::{fs::File, io::AsyncWriteExt, process::Command};
use crate::{consts::UPLOAD_MAX_SIZE, error::ErrorTypes, Data, Database}; use crate::{consts::UPLOAD_MAX_SIZE, error::ErrorTypes, Data, Database};
@ -37,12 +37,21 @@ pub trait SoundCtx {
user_id: U, user_id: U,
guild_id: G, guild_id: G,
) -> Result<Vec<Sound>, sqlx::Error>; ) -> Result<Vec<Sound>, sqlx::Error>;
async fn user_sounds<U: Into<u64> + Send>(&self, user_id: U) async fn user_sounds<U: Into<u64> + Send>(
-> Result<Vec<Sound>, sqlx::Error>; &self,
user_id: U,
page: Option<u64>,
) -> Result<Vec<Sound>, sqlx::Error>;
async fn guild_sounds<G: Into<u64> + Send>( async fn guild_sounds<G: Into<u64> + Send>(
&self, &self,
guild_id: G, guild_id: G,
page: Option<u64>,
) -> Result<Vec<Sound>, sqlx::Error>; ) -> Result<Vec<Sound>, sqlx::Error>;
async fn count_user_sounds<U: Into<u64> + Send>(&self, user_id: U) -> Result<u64, sqlx::Error>;
async fn count_guild_sounds<G: Into<u64> + Send>(
&self,
guild_id: G,
) -> Result<u64, sqlx::Error>;
} }
#[async_trait] #[async_trait]
@ -149,7 +158,7 @@ SELECT name, id, public, server_id, uploader_id
query: &str, query: &str,
user_id: U, user_id: U,
guild_id: G, guild_id: G,
) -> Result<Vec<Sound>, Error> { ) -> Result<Vec<Sound>, sqlx::Error> {
let db_pool = self.database.clone(); let db_pool = self.database.clone();
sqlx::query_as_unchecked!( sqlx::query_as_unchecked!(
@ -171,18 +180,41 @@ LIMIT 25
async fn user_sounds<U: Into<u64> + Send>( async fn user_sounds<U: Into<u64> + Send>(
&self, &self,
user_id: U, user_id: U,
page: Option<u64>,
) -> Result<Vec<Sound>, sqlx::Error> { ) -> Result<Vec<Sound>, sqlx::Error> {
let sounds = sqlx::query_as_unchecked!( let sounds = match page {
Some(page) => {
sqlx::query_as_unchecked!(
Sound, Sound,
" "
SELECT name, id, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
WHERE uploader_id = ? WHERE uploader_id = ?
ORDER BY name
LIMIT ?, ?
",
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() user_id.into()
) )
.fetch_all(&self.database) .fetch_all(&self.database)
.await?; .await?
}
};
Ok(sounds) Ok(sounds)
} }
@ -190,21 +222,68 @@ SELECT name, id, public, server_id, uploader_id
async fn guild_sounds<G: Into<u64> + Send>( async fn guild_sounds<G: Into<u64> + Send>(
&self, &self,
guild_id: G, guild_id: G,
page: Option<u64>,
) -> Result<Vec<Sound>, sqlx::Error> { ) -> Result<Vec<Sound>, sqlx::Error> {
let sounds = sqlx::query_as_unchecked!( let sounds = match page {
Some(page) => {
sqlx::query_as_unchecked!(
Sound, Sound,
" "
SELECT name, id, public, server_id, uploader_id SELECT name, id, public, server_id, uploader_id
FROM sounds FROM sounds
WHERE server_id = ? WHERE server_id = ?
ORDER BY name
LIMIT ?, ?
",
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() guild_id.into()
) )
.fetch_all(&self.database) .fetch_all(&self.database)
.await?; .await?
}
};
Ok(sounds) Ok(sounds)
} }
async fn count_user_sounds<U: Into<u64> + Send>(&self, user_id: U) -> Result<u64, sqlx::Error> {
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<G: Into<u64> + Send>(
&self,
guild_id: G,
) -> Result<u64, sqlx::Error> {
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 { impl Sound {
@ -262,7 +341,7 @@ SELECT src
pub async fn count_user_sounds<U: Into<u64>>( pub async fn count_user_sounds<U: Into<u64>>(
user_id: U, user_id: U,
db_pool: impl Executor<'_, Database = Database>, db_pool: impl Executor<'_, Database = Database>,
) -> Result<u32, sqlx::error::Error> { ) -> Result<u32, sqlx::Error> {
let user_id = user_id.into(); let user_id = user_id.into();
let c = sqlx::query!( let c = sqlx::query!(
@ -284,7 +363,7 @@ SELECT COUNT(1) as count
user_id: U, user_id: U,
name: &String, name: &String,
db_pool: impl Executor<'_, Database = Database>, db_pool: impl Executor<'_, Database = Database>,
) -> Result<u32, sqlx::error::Error> { ) -> Result<u32, sqlx::Error> {
let user_id = user_id.into(); let user_id = user_id.into();
let c = sqlx::query!( let c = sqlx::query!(