Favorite/unfavorite sounds

This commit is contained in:
jude 2023-08-16 16:47:33 +01:00
parent 92d8d077df
commit e875038851
9 changed files with 238 additions and 1 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="dataSourceStorageLocal" created-in="CL-231.9161.40"> <component name="dataSourceStorageLocal" created-in="CL-231.8109.174">
<data-source name="MySQL for 5.1 - soundfx@localhost" uuid="1067c1d0-1386-4a39-b3f5-6d48d6f279eb"> <data-source name="MySQL for 5.1 - soundfx@localhost" uuid="1067c1d0-1386-4a39-b3f5-6d48d6f279eb">
<database-info product="" version="" jdbc-version="" driver-name="" driver-version="" dbms="MYSQL" exact-version="0" /> <database-info product="" version="" jdbc-version="" driver-name="" driver-version="" dbms="MYSQL" exact-version="0" />
<secret-storage>master_key</secret-storage> <secret-storage>master_key</secret-storage>

View File

@ -0,0 +1,8 @@
ALTER TABLE users ADD PRIMARY KEY (`user`);
CREATE TABLE favorite_sounds (
user_id BIGINT UNSIGNED NOT NULL,
sound_id INT UNSIGNED NOT NULL,
FOREIGN KEY (sound_id) REFERENCES `sounds`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (user_id, sound_id)
);

96
src/cmds/favorite.rs Normal file
View File

@ -0,0 +1,96 @@
use log::warn;
use crate::{cmds::autocomplete_sound, models::sound::SoundCtx, Context, Error};
#[poise::command(slash_command, rename = "favorites", guild_only = true)]
pub async fn favorites(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// Add a sound as a favorite
#[poise::command(
slash_command,
rename = "add",
category = "Favorites",
guild_only = true
)]
pub async fn add_favorite(
ctx: Context<'_>,
#[description = "Name or ID of sound to favorite"]
#[autocomplete = "autocomplete_sound"]
name: String,
) -> Result<(), Error> {
let sounds = ctx
.data()
.search_for_sound(&name, ctx.guild_id().unwrap(), ctx.author().id, true)
.await;
match sounds {
Ok(sounds) => {
let sound = &sounds[0];
sound
.add_favorite(ctx.author().id, &ctx.data().database)
.await?;
ctx.say(format!(
"Sound {} (ID {}) added to favorites.",
sound.name, sound.id
))
.await?;
Ok(())
}
Err(e) => {
warn!("Couldn't fetch sounds: {:?}", e);
ctx.say("Failed to find sound.").await?;
Ok(())
}
}
}
/// Remove a sound from your favorites
#[poise::command(
slash_command,
rename = "remove",
category = "Favorites",
guild_only = true
)]
pub async fn remove_favorite(
ctx: Context<'_>,
#[description = "Name or ID of sound to favorite"]
#[autocomplete = "autocomplete_sound"]
name: String,
) -> Result<(), Error> {
let sounds = ctx
.data()
.search_for_sound(&name, ctx.guild_id().unwrap(), ctx.author().id, true)
.await;
match sounds {
Ok(sounds) => {
let sound = &sounds[0];
sound
.remove_favorite(ctx.author().id, &ctx.data().database)
.await?;
ctx.say(format!(
"Sound {} (ID {}) removed from favorites.",
sound.name, sound.id
))
.await?;
Ok(())
}
Err(e) => {
warn!("Couldn't fetch sounds: {:?}", e);
ctx.say("Failed to find sound.").await?;
Ok(())
}
}
}

View File

@ -33,6 +33,9 @@ __Library Commands__
`/public` - Set a sound as public/private `/public` - Set a sound as public/private
`/list server` - List sounds on this server `/list server` - List sounds on this server
`/list user` - List your sounds `/list user` - List your sounds
`/favorites add` - Add a favorite
`/favorites remove` - Remove a favorite
`/list favorites` - List favorites
__Search Commands__ __Search Commands__
`/search` - Search for public sounds by name `/search` - Search for public sounds by name

View File

@ -1,5 +1,6 @@
use crate::{models::sound::SoundCtx, Context}; use crate::{models::sound::SoundCtx, Context};
pub mod favorite;
pub mod info; pub mod info;
pub mod manage; pub mod manage;
pub mod play; pub mod play;

View File

@ -47,12 +47,14 @@ pub async fn list_sounds(_ctx: Context<'_>) -> Result<(), Error> {
enum ListContext { enum ListContext {
User = 0, User = 0,
Guild = 1, Guild = 1,
Favorite = 2,
} }
impl ListContext { impl ListContext {
pub fn title(&self) -> &'static str { pub fn title(&self) -> &'static str {
match self { match self {
ListContext::User => "Your sounds", ListContext::User => "Your sounds",
ListContext::Favorite => "Your favorite sounds",
ListContext::Guild => "Server sounds", ListContext::Guild => "Server sounds",
} }
} }
@ -86,6 +88,20 @@ pub async fn list_user_sounds(ctx: Context<'_>) -> Result<(), Error> {
Ok(()) Ok(())
} }
/// Show all sounds you have uploaded
#[poise::command(slash_command, rename = "user", guild_only = true)]
pub async fn list_favorite_sounds(ctx: Context<'_>) -> Result<(), Error> {
let pager = SoundPager {
nonce: 0,
page: 0,
context: ListContext::Favorite,
};
pager.reply(ctx).await?;
Ok(())
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct SoundPager { pub struct SoundPager {
nonce: u64, nonce: u64,
@ -102,6 +118,7 @@ impl SoundPager {
) -> Result<Vec<Sound>, sqlx::Error> { ) -> Result<Vec<Sound>, sqlx::Error> {
match self.context { match self.context {
ListContext::User => data.user_sounds(user_id, Some(self.page)).await, ListContext::User => data.user_sounds(user_id, Some(self.page)).await,
ListContext::Favorite => data.favorite_sounds(user_id, Some(self.page)).await,
ListContext::Guild => data.guild_sounds(guild_id, Some(self.page)).await, ListContext::Guild => data.guild_sounds(guild_id, Some(self.page)).await,
} }
} }
@ -205,6 +222,7 @@ impl SoundPager {
let sounds = pager.get_page(data, user_id, guild_id).await?; let sounds = pager.get_page(data, user_id, guild_id).await?;
let count = match pager.context { let count = match pager.context {
ListContext::User => data.count_user_sounds(user_id).await?, ListContext::User => data.count_user_sounds(user_id).await?,
ListContext::Favorite => data.count_favorite_sounds(user_id).await?,
ListContext::Guild => data.count_guild_sounds(guild_id).await?, ListContext::Guild => data.count_guild_sounds(guild_id).await?,
}; };
@ -228,6 +246,7 @@ impl SoundPager {
.await?; .await?;
let count = match self.context { let count = match self.context {
ListContext::User => ctx.data().count_user_sounds(ctx.author().id).await?, ListContext::User => ctx.data().count_user_sounds(ctx.author().id).await?,
ListContext::Favorite => ctx.data().count_favorite_sounds(ctx.author().id).await?,
ListContext::Guild => { ListContext::Guild => {
ctx.data() ctx.data()
.count_guild_sounds(ctx.guild_id().unwrap()) .count_guild_sounds(ctx.guild_id().unwrap())

View File

@ -92,9 +92,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
subcommands: vec![ subcommands: vec![
cmds::search::list_guild_sounds(), cmds::search::list_guild_sounds(),
cmds::search::list_user_sounds(), cmds::search::list_user_sounds(),
cmds::search::list_favorite_sounds(),
], ],
..cmds::search::list_sounds() ..cmds::search::list_sounds()
}, },
poise::Command {
subcommands: vec![
cmds::favorite::add_favorite(),
cmds::favorite::remove_favorite(),
],
..cmds::favorite::favorites()
},
cmds::search::show_random_sounds(), cmds::search::show_random_sounds(),
cmds::search::search_sounds(), cmds::search::search_sounds(),
cmds::stop::stop_playing(), cmds::stop::stop_playing(),

View File

@ -42,12 +42,21 @@ pub trait SoundCtx {
user_id: U, user_id: U,
page: Option<u64>, page: Option<u64>,
) -> Result<Vec<Sound>, sqlx::Error>; ) -> Result<Vec<Sound>, sqlx::Error>;
async fn favorite_sounds<U: Into<u64> + Send>(
&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>, 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_user_sounds<U: Into<u64> + Send>(&self, user_id: U) -> Result<u64, sqlx::Error>;
async fn count_favorite_sounds<U: Into<u64> + Send>(
&self,
user_id: U,
) -> Result<u64, sqlx::Error>;
async fn count_guild_sounds<G: Into<u64> + Send>( async fn count_guild_sounds<G: Into<u64> + Send>(
&self, &self,
guild_id: G, guild_id: G,
@ -219,6 +228,50 @@ SELECT name, id, public, server_id, uploader_id
Ok(sounds) Ok(sounds)
} }
async fn favorite_sounds<U: Into<u64> + Send>(
&self,
user_id: U,
page: Option<u64>,
) -> Result<Vec<Sound>, sqlx::Error> {
let sounds = match page {
Some(page) => {
sqlx::query_as_unchecked!(
Sound,
"
SELECT name, id, public, server_id, uploader_id
FROM sounds
INNER JOIN favorite_sounds f ON sounds.id = f.sound_id
WHERE f.user_id = ?
ORDER BY id DESC
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
INNER JOIN favorite_sounds f ON sounds.id = f.sound_id
WHERE f.user_id = ?
ORDER BY id DESC
",
user_id.into()
)
.fetch_all(&self.database)
.await?
}
};
Ok(sounds)
}
async fn guild_sounds<G: Into<u64> + Send>( async fn guild_sounds<G: Into<u64> + Send>(
&self, &self,
guild_id: G, guild_id: G,
@ -272,6 +325,19 @@ SELECT name, id, public, server_id, uploader_id
.count as u64) .count as u64)
} }
async fn count_favorite_sounds<U: Into<u64> + Send>(
&self,
user_id: U,
) -> Result<u64, sqlx::Error> {
Ok(sqlx::query!(
"SELECT COUNT(1) as count FROM favorite_sounds WHERE user_id = ?",
user_id.into()
)
.fetch_one(&self.database)
.await?
.count as u64)
}
async fn count_guild_sounds<G: Into<u64> + Send>( async fn count_guild_sounds<G: Into<u64> + Send>(
&self, &self,
guild_id: G, guild_id: G,
@ -416,6 +482,42 @@ WHERE
Ok(()) Ok(())
} }
pub async fn add_favorite<U: Into<u64>>(
&self,
user_id: U,
db_pool: impl Executor<'_, Database = Database>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync + Send>> {
let user_id = user_id.into();
sqlx::query!(
"INSERT INTO favorite_sounds (user_id, sound_id) VALUES (?, ?)",
user_id,
self.id
)
.execute(db_pool)
.await?;
Ok(())
}
pub async fn remove_favorite<U: Into<u64>>(
&self,
user_id: U,
db_pool: impl Executor<'_, Database = Database>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync + Send>> {
let user_id = user_id.into();
sqlx::query!(
"DELETE FROM favorite_sounds WHERE user_id = ? AND sound_id = ?",
user_id,
self.id
)
.execute(db_pool)
.await?;
Ok(())
}
pub async fn create_anon<G: Into<u64>, U: Into<u64>>( pub async fn create_anon<G: Into<u64>, U: Into<u64>>(
name: &str, name: &str,
src_url: &str, src_url: &str,