From b436b079557a9c4d20336d8e397c915cb74468f8 Mon Sep 17 00:00:00 2001 From: kasisnu <5044120+kasisnu@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:38:29 +0530 Subject: [PATCH 1/5] super basic favorites impl --- .../2023-11-17-094126_favorite/down.sql | 5 + .../2023-11-17-094126_favorite/up.sql | 59 +++++++ .../favorites/favorite_entity_type.rs | 101 +++++++++++ .../enums/src/by_table/favorites/mod.rs | 1 + crates/_types/enums/src/by_table/mod.rs | 1 + crates/_types/tokens/src/prefixes.rs | 2 + crates/_types/tokens/src/tokens/favorites.rs | 14 ++ crates/_types/tokens/src/tokens/mod.rs | 1 + .../src/queries/favorites/delete_favorite.rs | 55 ++++++ .../favorites/favorite_entity_token.rs | 36 ++++ .../src/queries/favorites/get_favorite.rs | 114 +++++++++++++ .../src/queries/favorites/insert_favorite.rs | 67 ++++++++ .../favorites/list_favorites_for_entity.rs | 116 +++++++++++++ .../src/queries/favorites/mod.rs | 5 + crates/lib/mysql_queries/src/queries/mod.rs | 3 +- .../favorites/create_favorite_handler.rs | 141 +++++++++++++++ .../favorites/delete_favorite_handler.rs | 152 +++++++++++++++++ .../favorites/list_favorites_handler.rs | 160 ++++++++++++++++++ .../http_server/endpoints/favorites/mod.rs | 3 + .../src/http_server/endpoints/mod.rs | 1 + .../service/web/storyteller_web/src/routes.rs | 11 ++ 21 files changed, 1047 insertions(+), 1 deletion(-) create mode 100644 _sql/migrations/2023-11-17-094126_favorite/down.sql create mode 100644 _sql/migrations/2023-11-17-094126_favorite/up.sql create mode 100644 crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs create mode 100644 crates/_types/enums/src/by_table/favorites/mod.rs create mode 100644 crates/_types/tokens/src/tokens/favorites.rs create mode 100644 crates/lib/mysql_queries/src/queries/favorites/delete_favorite.rs create mode 100644 crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs create mode 100644 crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs create mode 100644 crates/lib/mysql_queries/src/queries/favorites/insert_favorite.rs create mode 100644 crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs create mode 100644 crates/lib/mysql_queries/src/queries/favorites/mod.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/favorites/delete_favorite_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/favorites/mod.rs diff --git a/_sql/migrations/2023-11-17-094126_favorite/down.sql b/_sql/migrations/2023-11-17-094126_favorite/down.sql new file mode 100644 index 0000000000..26a2a6d8fc --- /dev/null +++ b/_sql/migrations/2023-11-17-094126_favorite/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE favorites; \ No newline at end of file diff --git a/_sql/migrations/2023-11-17-094126_favorite/up.sql b/_sql/migrations/2023-11-17-094126_favorite/up.sql new file mode 100644 index 0000000000..429c8bacf4 --- /dev/null +++ b/_sql/migrations/2023-11-17-094126_favorite/up.sql @@ -0,0 +1,59 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Favorites that can be attached to just about anything. +-- This is a polymorphic table and can apply to a wide variety of entities. +CREATE TABLE favorites +( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + token VARCHAR(32) NOT NULL, + + -- Idempotency token from client + -- This is so the frontend client doesn't submit duplicate items. + uuid_idempotency_token VARCHAR(36) NOT NULL, + + -- User making the favorite + user_token VARCHAR(32) NOT NULL, + + -- The type of entity the favorite applies to + entity_type VARCHAR(32) NOT NULL, + + -- The token of the entity the favorite applies to + entity_token VARCHAR(32) NOT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + creator_ip_address VARCHAR(40) NOT NULL, + + -- ========== VECTOR CLOCK ========== + + -- Incremented with every update. + version INT NOT NULL DEFAULT 0, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- If this is removed by a mod. + -- It completely disappears from the system. + user_deleted_at TIMESTAMP NULL, + mod_deleted_at TIMESTAMP NULL, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + UNIQUE KEY (uuid_idempotency_token), + KEY index_entity_type_entity_token (entity_type, entity_token), + KEY index_user_token (user_token), + KEY index_created_at (created_at), + KEY index_user_deleted_at (user_deleted_at), + KEY index_mod_deleted_at (mod_deleted_at) + +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_bin; diff --git a/crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs b/crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs new file mode 100644 index 0000000000..67f1b88e13 --- /dev/null +++ b/crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs @@ -0,0 +1,101 @@ +use serde::Deserialize; +use serde::Serialize; +#[cfg(test)] +use strum::EnumCount; +#[cfg(test)] +use strum::EnumIter; + +/// Used in the `favorites` table in a `VARCHAR(32)` field named `entity_type`. +#[cfg_attr(test, derive(EnumIter, EnumCount))] +#[derive(Clone, Copy, Eq, PartialEq, Hash, Deserialize, Serialize)] +pub enum FavoriteEntityType { + /// User + #[serde(rename = "user")] + User, + + /// TTS model (architecture does not matter) + #[serde(rename = "tts_model")] + TtsModel, + + /// TTS result (architecture does not matter) + #[serde(rename = "tts_result")] + TtsResult, + + /// W2L template + #[serde(rename = "w2l_template")] + W2lTemplate, + + /// W2L result + #[serde(rename = "w2l_result")] + W2lResult, +} + +// TODO(bt, 2023-01-17): This desperately needs MySQL integration tests! +impl_enum_display_and_debug_using_to_str!(FavoriteEntityType); +impl_mysql_enum_coders!(FavoriteEntityType); + +/// NB: Legacy API for older code. +impl FavoriteEntityType { + pub fn to_str(&self) -> &'static str { + match self { + Self::User => "user", + Self::TtsModel => "tts_model", + Self::TtsResult => "tts_result", + Self::W2lTemplate => "w2l_template", + Self::W2lResult => "w2l_result", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "user" => Ok(Self::User), + "tts_model" => Ok(Self::TtsModel), + "tts_result" => Ok(Self::TtsResult), + "w2l_template" => Ok(Self::W2lTemplate), + "w2l_result" => Ok(Self::W2lResult), + _ => Err(format!("invalid value: {:?}", value)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::by_table::favorites::favorite_entity_type::FavoriteEntityType; + use crate::test_helpers::assert_serialization; + + mod serde { + use super::*; + + #[test] + fn test_serialization() { + assert_serialization(FavoriteEntityType::User, "user"); + assert_serialization(FavoriteEntityType::TtsModel, "tts_model"); + assert_serialization(FavoriteEntityType::TtsResult, "tts_result"); + assert_serialization(FavoriteEntityType::W2lTemplate, "w2l_template"); + assert_serialization(FavoriteEntityType::W2lResult, "w2l_result"); + } + } + + mod impl_methods { + use super::*; + + #[test] + fn test_to_str() { + assert_eq!(FavoriteEntityType::User.to_str(), "user"); + assert_eq!(FavoriteEntityType::TtsModel.to_str(), "tts_model"); + assert_eq!(FavoriteEntityType::TtsResult.to_str(), "tts_result"); + assert_eq!(FavoriteEntityType::W2lTemplate.to_str(), "w2l_template"); + assert_eq!(FavoriteEntityType::W2lResult.to_str(), "w2l_result"); + } + + #[test] + fn test_from_str() { + assert_eq!(FavoriteEntityType::from_str("user").unwrap(), FavoriteEntityType::User); + assert_eq!(FavoriteEntityType::from_str("tts_model").unwrap(), FavoriteEntityType::TtsModel); + assert_eq!(FavoriteEntityType::from_str("tts_result").unwrap(), FavoriteEntityType::TtsResult); + assert_eq!(FavoriteEntityType::from_str("w2l_template").unwrap(), FavoriteEntityType::W2lTemplate); + assert_eq!(FavoriteEntityType::from_str("w2l_result").unwrap(), FavoriteEntityType::W2lResult); + assert!(FavoriteEntityType::from_str("foo").is_err()); + } + } +} diff --git a/crates/_types/enums/src/by_table/favorites/mod.rs b/crates/_types/enums/src/by_table/favorites/mod.rs new file mode 100644 index 0000000000..18ac93ed9c --- /dev/null +++ b/crates/_types/enums/src/by_table/favorites/mod.rs @@ -0,0 +1 @@ +pub mod favorite_entity_type; \ No newline at end of file diff --git a/crates/_types/enums/src/by_table/mod.rs b/crates/_types/enums/src/by_table/mod.rs index 1061281e12..6226efae12 100644 --- a/crates/_types/enums/src/by_table/mod.rs +++ b/crates/_types/enums/src/by_table/mod.rs @@ -19,3 +19,4 @@ pub mod zs_voices; pub mod tts_render_tasks; pub mod web_rendition_targets; pub mod web_scraping_targets; +pub mod favorites; diff --git a/crates/_types/tokens/src/prefixes.rs b/crates/_types/tokens/src/prefixes.rs index 41b6217e91..6e24a9802b 100644 --- a/crates/_types/tokens/src/prefixes.rs +++ b/crates/_types/tokens/src/prefixes.rs @@ -27,6 +27,7 @@ pub(crate) enum TokenPrefix { Comment, DownloadJob, EmailSenderJob, + Favorite, InferenceJob, MediaFile, MediaUpload, @@ -93,6 +94,7 @@ impl PrefixGenerator for TokenPrefix { Self::DownloadJob => "jdown_", // NB: Previously "JGUP:" Self::EmailSenderJob => "email_job_", Self::InferenceJob => "jinf_", + Self::Favorite => "fav_", Self::MediaFile => "m_", Self::MediaUpload => "mu_", Self::ModelWeight => "weight_", diff --git a/crates/_types/tokens/src/tokens/favorites.rs b/crates/_types/tokens/src/tokens/favorites.rs new file mode 100644 index 0000000000..78ecb7e0b9 --- /dev/null +++ b/crates/_types/tokens/src/tokens/favorites.rs @@ -0,0 +1,14 @@ +use std::fmt::Debug; + +use serde::Deserialize; +use serde::Serialize; +use crate::prefixes::TokenPrefix; + + +/// The primary key for Audit Logs. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Debug, Serialize, Deserialize)] +#[sqlx(transparent)] +pub struct FavoriteToken(pub String); + +impl_string_token!(FavoriteToken); +impl_crockford_generator!(FavoriteToken, 32usize, TokenPrefix::Favorite, CrockfordLower); diff --git a/crates/_types/tokens/src/tokens/mod.rs b/crates/_types/tokens/src/tokens/mod.rs index 1777a47e2f..e75f732b55 100644 --- a/crates/_types/tokens/src/tokens/mod.rs +++ b/crates/_types/tokens/src/tokens/mod.rs @@ -34,3 +34,4 @@ pub mod w2l_templates; pub mod zs_voice_dataset_samples; pub mod zs_voice_datasets; pub mod zs_voices; +pub mod favorites; diff --git a/crates/lib/mysql_queries/src/queries/favorites/delete_favorite.rs b/crates/lib/mysql_queries/src/queries/favorites/delete_favorite.rs new file mode 100644 index 0000000000..3d0df0b3b0 --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/favorites/delete_favorite.rs @@ -0,0 +1,55 @@ +use sqlx::{Executor, MySql}; + +use errors::AnyhowResult; +use tokens::tokens::favorites::FavoriteToken; + +#[derive(Copy, Clone)] +pub enum DeleteFavoriteAs { + Author, + Moderator, +} + +pub async fn delete_favorite<'e, 'c, E>( + favorite_token: &'e FavoriteToken, + delete_as: DeleteFavoriteAs, + mysql_executor: E +) + -> AnyhowResult<()> + where E: 'e + Executor<'c, Database = MySql> +{ + + match delete_as { + DeleteFavoriteAs::Author => { + sqlx::query!( + r#" +UPDATE favorites +SET + user_deleted_at = CURRENT_TIMESTAMP, + version = version + 1 +WHERE + token = ? +LIMIT 1 + "#, + favorite_token + ) + } + DeleteFavoriteAs::Moderator => { + sqlx::query!( + r#" +UPDATE favorites +SET + mod_deleted_at = CURRENT_TIMESTAMP, + version = version + 1 +WHERE + token = ? +LIMIT 1 + "#, + favorite_token + ) + } + } + .execute(mysql_executor) + .await?; + + Ok(()) +} diff --git a/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs b/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs new file mode 100644 index 0000000000..f62fb91cef --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs @@ -0,0 +1,36 @@ +use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; +use tokens::tokens::tts_models::TtsModelToken; +use tokens::tokens::w2l_templates::W2lTemplateToken; +use tokens::tokens::users::UserToken; + +pub enum FavoriteEntityToken { + User(UserToken), + TtsModel(TtsModelToken), + TtsResult(String), // TODO: Strong type + W2lTemplate(W2lTemplateToken), + W2lResult(String), // TODO: Strong type +} + +impl FavoriteEntityToken { + + pub fn from_entity_type_and_token(entity_type: FavoriteEntityType, token: &str) -> Self { + match entity_type { + FavoriteEntityType::User => Self::User(UserToken::new_from_str(token)), + FavoriteEntityType::TtsModel => Self::TtsModel(TtsModelToken::new_from_str(token)), + FavoriteEntityType::TtsResult => Self::TtsResult(token.to_string()), + FavoriteEntityType::W2lTemplate => Self::W2lTemplate(W2lTemplateToken::new_from_str(token)), + FavoriteEntityType::W2lResult => Self::W2lResult(token.to_string()), + } + } + + pub fn get_composite_keys(&self) -> (FavoriteEntityType, &str) { + let (entity_type, entity_token) = match self { + FavoriteEntityToken::User(user_token) => (FavoriteEntityType::User, user_token.as_str()), + FavoriteEntityToken::TtsModel(tts_model_token) => (FavoriteEntityType::TtsModel, tts_model_token.as_str()), + FavoriteEntityToken::TtsResult(tts_result_token) => (FavoriteEntityType::TtsResult, tts_result_token.as_str()), + FavoriteEntityToken::W2lTemplate(w2l_template_token) => (FavoriteEntityType::W2lTemplate, w2l_template_token.as_str()), + FavoriteEntityToken::W2lResult(w2l_result_token) => (FavoriteEntityType::W2lResult, w2l_result_token.as_str()), + }; + (entity_type, entity_token) + } +} diff --git a/crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs b/crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs new file mode 100644 index 0000000000..9360aa8fa7 --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs @@ -0,0 +1,114 @@ +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use sqlx::{Executor, MySql}; + +use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; +use errors::AnyhowResult; +use tokens::tokens::favorites::FavoriteToken; +use tokens::tokens::users::UserToken; + +pub struct Favorite { + pub token: FavoriteToken, + + pub entity_type: FavoriteEntityType, + pub entity_token: String, + + pub user_token: UserToken, + pub username: String, + pub user_display_name: String, + pub user_gravatar_hash: String, + + pub mod_fields: FavoriteForListModFields, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +pub struct FavoriteForListModFields { + pub creator_ip_address: String, + pub maybe_user_deleted_at: Option>, + pub maybe_mod_deleted_at: Option>, +} + +pub async fn get_favorite<'e, 'c, E>( + favorite_token: &'e FavoriteToken, + mysql_executor: E +) + -> AnyhowResult> + where E: 'e + Executor<'c, Database = MySql> +{ + + let maybe_results = sqlx::query_as!( + RawFavorite, + r#" +SELECT + f.token as `token: tokens::tokens::favorites::FavoriteToken`, + + f.entity_type as `entity_type: enums::by_table::favorites::favorite_entity_type::FavoriteEntityType`, + f.entity_token, + + f.user_token as `user_token: tokens::tokens::users::UserToken`, + u.username, + u.display_name as user_display_name, + u.email_gravatar_hash as user_gravatar_hash, + + f.creator_ip_address, + f.created_at, + f.updated_at, + f.user_deleted_at, + f.mod_deleted_at + +FROM + favorites AS f +JOIN users AS u + ON f.user_token = u.token +WHERE + f.token = ? + "#, + favorite_token + ) + .fetch_one(mysql_executor) + .await; + + match maybe_results { + Ok(favorite) => Ok(Some(Favorite { + token: favorite.token, + entity_type: favorite.entity_type, + entity_token: favorite.entity_token, + user_token: favorite.user_token, + username: favorite.username, + user_display_name: favorite.user_display_name, + user_gravatar_hash: favorite.user_gravatar_hash, + mod_fields: FavoriteForListModFields { + creator_ip_address: favorite.creator_ip_address, + maybe_user_deleted_at: favorite.user_deleted_at, + maybe_mod_deleted_at: favorite.mod_deleted_at, + }, + created_at: favorite.created_at, + updated_at: favorite.updated_at, + })), + Err(err) => match err { + sqlx::Error::RowNotFound => Ok(None), + _ => Err(anyhow!("Error querying for IP ban: {:?}", err)), + } + } +} + +pub struct RawFavorite { + pub token: FavoriteToken, + + pub entity_type: FavoriteEntityType, + pub entity_token: String, + + pub user_token: UserToken, + pub username: String, + pub user_display_name: String, + pub user_gravatar_hash: String, + + pub creator_ip_address: String, + + pub created_at: DateTime, + pub updated_at: DateTime, + pub user_deleted_at: Option>, + pub mod_deleted_at: Option>, +} diff --git a/crates/lib/mysql_queries/src/queries/favorites/insert_favorite.rs b/crates/lib/mysql_queries/src/queries/favorites/insert_favorite.rs new file mode 100644 index 0000000000..8e12c2dffe --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/favorites/insert_favorite.rs @@ -0,0 +1,67 @@ +use std::marker::PhantomData; + +use anyhow::anyhow; +use sqlx::{Executor, MySql}; + +use errors::AnyhowResult; +use tokens::tokens::favorites::FavoriteToken; +use tokens::tokens::users::UserToken; + +use crate::queries::favorites::favorite_entity_token::FavoriteEntityToken; + +pub struct InsertFavoriteArgs<'e, 'c, E> + where E: 'e + Executor<'c, Database = MySql> +{ + pub entity_token: &'e FavoriteEntityToken, + + pub uuid_idempotency_token: &'e str, + + pub user_token: &'e UserToken, + + pub creator_ip_address: &'e str, + + pub mysql_executor: E, + + // TODO: Not sure if this works to tell the compiler we need the lifetime annotation. + // See: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-lifetime-parameters + pub phantom: PhantomData<&'c E>, +} + +pub async fn insert_favorite<'e, 'c : 'e, E>( + args: InsertFavoriteArgs<'e, 'c, E>, +) + -> AnyhowResult + where E: 'e + Executor<'c, Database = MySql> +{ + + let favorite_token = FavoriteToken::generate(); + let (entity_type, entity_token) = args.entity_token.get_composite_keys(); + + let query_result = sqlx::query!( + r#" +INSERT INTO favorites +SET + token = ?, + uuid_idempotency_token = ?, + user_token = ?, + entity_type = ?, + entity_token = ?, + creator_ip_address = ? + "#, + &favorite_token, + args.uuid_idempotency_token, + args.user_token, + entity_type, + entity_token, + args.creator_ip_address, + ) + .execute(args.mysql_executor) + .await; + + let _record_id = match query_result { + Ok(res) => res.last_insert_id(), + Err(err) => return Err(anyhow!("Mysql error: {:?}", err)), + }; + + Ok(favorite_token) +} diff --git a/crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs b/crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs new file mode 100644 index 0000000000..a0d77aa3f1 --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs @@ -0,0 +1,116 @@ +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use log::warn; +use sqlx::MySqlPool; + +use errors::AnyhowResult; +use tokens::tokens::favorites::FavoriteToken; +use tokens::tokens::users::UserToken; + +use crate::queries::favorites::favorite_entity_token::FavoriteEntityToken; + +pub struct FavoriteForList { + pub token: FavoriteToken, + + pub user_token: UserToken, + pub username: String, + pub user_display_name: String, + pub user_gravatar_hash: String, + + pub mod_fields: FavoriteForListModFields, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +pub struct FavoriteForListModFields { + pub creator_ip_address: String, + pub maybe_user_deleted_at: Option>, + pub maybe_mod_deleted_at: Option>, +} + +pub async fn list_favorites_for_entity( + favorite_entity_token: FavoriteEntityToken, + mysql_pool: &MySqlPool +) -> AnyhowResult> { + + let (entity_type, entity_token) = favorite_entity_token.get_composite_keys(); + + let maybe_results= sqlx::query_as!( + RawFavoriteForList, + r#" +SELECT + f.token as `token: tokens::tokens::favorites::FavoriteToken`, + f.user_token as `user_token: tokens::tokens::users::UserToken`, + u.username, + u.display_name as user_display_name, + u.email_gravatar_hash as user_gravatar_hash, + + f.creator_ip_address, + + f.created_at, + f.updated_at, + f.user_deleted_at, + f.mod_deleted_at + +FROM + favorites AS f +JOIN users AS u + ON f.user_token = u.token +WHERE + f.entity_type = ? + AND f.entity_token = ? + AND f.user_deleted_at IS NULL + AND f.mod_deleted_at IS NULL +ORDER BY f.id DESC +LIMIT 50 + "#, + entity_type, + entity_token + ) + .fetch_all(mysql_pool) + .await; + + match maybe_results { + Err(err) => match err { + sqlx::Error::RowNotFound => Ok(Vec::new()), + _ => { + warn!("list ip bans db error: {:?}", err); + Err(anyhow!("error with query: {:?}", err)) + } + }, + Ok(results) => Ok(results.into_iter() + .map(|favorite| FavoriteForList { + token: favorite.token, + user_token: favorite.user_token, + username: favorite.username, + user_display_name: favorite.user_display_name, + user_gravatar_hash: favorite.user_gravatar_hash, + mod_fields: FavoriteForListModFields { + creator_ip_address: favorite.creator_ip_address, + maybe_user_deleted_at: favorite.user_deleted_at, + maybe_mod_deleted_at: favorite.mod_deleted_at, + }, + created_at: favorite.created_at, + updated_at: favorite.updated_at, + }) + .collect()), + } +} + +pub struct RawFavoriteForList { + pub token: FavoriteToken, + + pub user_token: UserToken, + pub username: String, + pub user_display_name: String, + pub user_gravatar_hash: String, + + + pub creator_ip_address: String, + + pub created_at: DateTime, + pub updated_at: DateTime, + pub user_deleted_at: Option>, + pub mod_deleted_at: Option>, +} diff --git a/crates/lib/mysql_queries/src/queries/favorites/mod.rs b/crates/lib/mysql_queries/src/queries/favorites/mod.rs new file mode 100644 index 0000000000..7143c80f04 --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/favorites/mod.rs @@ -0,0 +1,5 @@ +pub mod favorite_entity_token; +pub mod delete_favorite; +pub mod get_favorite; +pub mod insert_favorite; +pub mod list_favorites_for_entity; \ No newline at end of file diff --git a/crates/lib/mysql_queries/src/queries/mod.rs b/crates/lib/mysql_queries/src/queries/mod.rs index 523ec28d2f..8d216155ef 100644 --- a/crates/lib/mysql_queries/src/queries/mod.rs +++ b/crates/lib/mysql_queries/src/queries/mod.rs @@ -3,6 +3,7 @@ pub mod audit_logs; pub mod billing; pub mod comments; pub mod email_sender_jobs; +pub mod favorites; pub mod generic_download; pub mod generic_inference; pub mod generic_synthetic_ids; @@ -21,4 +22,4 @@ pub mod vocoder; pub mod voice_clone_requests; pub mod voice_conversion; pub mod voice_designer; -pub mod w2l; +pub mod w2l; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs new file mode 100644 index 0000000000..399aa8e570 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs @@ -0,0 +1,141 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use std::fmt; +use std::sync::Arc; + +use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use log::warn; + +use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; +use http_server_common::request::get_request_ip::get_request_ip; +use mysql_queries::queries::favorites::favorite_entity_token::FavoriteEntityToken; +use mysql_queries::queries::favorites::insert_favorite::{insert_favorite, InsertFavoriteArgs}; +use tokens::tokens::favorites::FavoriteToken; +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::server_state::ServerState; + +#[derive(Deserialize)] +pub struct CreateFavoriteRequest { + uuid_idempotency_token: String, + entity_token: String, + entity_type: FavoriteEntityType, +} + +#[derive(Serialize)] +pub struct CreateFavoriteSuccessResponse { + pub success: bool, + pub favorite_token: FavoriteToken, +} + +#[derive(Debug)] +pub enum CreateFavoriteError { + BadInput(String), + NotAuthorized, + ServerError, +} + +impl ResponseError for CreateFavoriteError { + fn status_code(&self) -> StatusCode { + match *self { + CreateFavoriteError::BadInput(_) => StatusCode::BAD_REQUEST, + CreateFavoriteError::NotAuthorized => StatusCode::UNAUTHORIZED, + CreateFavoriteError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + CreateFavoriteError::BadInput(reason) => reason.to_string(), + CreateFavoriteError::NotAuthorized => "unauthorized".to_string(), + CreateFavoriteError::ServerError => "server error".to_string(), + }; + + to_simple_json_error(&error_reason, self.status_code()) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for CreateFavoriteError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +pub async fn create_favorite_handler( + http_request: HttpRequest, + request: web::Json, + server_state: web::Data>, +) -> Result +{ + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await + .map_err(|err| { + warn!("MySql pool error: {:?}", err); + CreateFavoriteError::ServerError + })?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CreateFavoriteError::ServerError + })?; + + let user_session = match maybe_user_session { + Some(session) => session, + None => { + warn!("not logged in"); + return Err(CreateFavoriteError::NotAuthorized); + } + }; + + let ip_address = get_request_ip(&http_request); + + let entity_token = FavoriteEntityToken::from_entity_type_and_token( + request.entity_type, &request.entity_token); + + let query_result = insert_favorite(InsertFavoriteArgs { + entity_token: &entity_token, + uuid_idempotency_token: &request.uuid_idempotency_token, + user_token: &user_session.user_token_typed, + creator_ip_address: &ip_address, + mysql_executor: &mut mysql_connection, + phantom: Default::default(), + }).await; + + let favorite_token = match query_result { + Ok(token) => token, + Err(err) => { + warn!("error inserting favorite: {:?}", err); + return Err(CreateFavoriteError::ServerError); + } + }; + + // server_state.firehose_publisher.publish_favorite_created( + // &user_session.user_token_typed, &favorite_token) + // .await + // .map_err(|e| { + // warn!("error publishing event: {:?}", e); + // CreateFavoriteError::ServerError + // })?; + + let response = CreateFavoriteSuccessResponse { + success: true, + favorite_token, + }; + + let body = serde_json::to_string(&response) + .map_err(|_e| CreateFavoriteError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/delete_favorite_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/delete_favorite_handler.rs new file mode 100644 index 0000000000..90e24f4941 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/delete_favorite_handler.rs @@ -0,0 +1,152 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use std::fmt; +use std::sync::Arc; + +use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Path; +use log::{error, warn}; + +use mysql_queries::queries::favorites::delete_favorite::{delete_favorite, DeleteFavoriteAs}; +use mysql_queries::queries::favorites::get_favorite::get_favorite; +use tokens::tokens::favorites::FavoriteToken; + +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::http_server::web_utils::response_success_helpers::simple_json_success; +use crate::server_state::ServerState; + +/// For the URL PathInfo +#[derive(Deserialize)] +pub struct DeleteFavoritePathInfo { + favorite_token: FavoriteToken, +} + +#[derive(Deserialize)] +pub struct DeleteFavoriteRequest { + /// NB: this is only to disambiguate when a user is both a mod and an author. + as_mod: Option, +} + +#[derive(Debug)] +pub enum DeleteFavoriteError { + BadInput(String), + NotAuthorized, + NotFound, + ServerError, +} + +impl ResponseError for DeleteFavoriteError { + fn status_code(&self) -> StatusCode { + match *self { + DeleteFavoriteError::BadInput(_) => StatusCode::BAD_REQUEST, + DeleteFavoriteError::NotAuthorized => StatusCode::UNAUTHORIZED, + DeleteFavoriteError::NotFound => StatusCode::NOT_FOUND, + DeleteFavoriteError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + DeleteFavoriteError::BadInput(reason) => reason.to_string(), + DeleteFavoriteError::NotAuthorized => "unauthorized".to_string(), + DeleteFavoriteError::NotFound => "not found".to_string(), + DeleteFavoriteError::ServerError => "server error".to_string(), + }; + + to_simple_json_error(&error_reason, self.status_code()) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for DeleteFavoriteError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +pub async fn delete_favorite_handler( + http_request: HttpRequest, + path: Path, + request: web::Json, + server_state: web::Data> +) -> Result { + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await + .map_err(|err| { + warn!("MySql pool error: {:?}", err); + DeleteFavoriteError::ServerError + })?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + DeleteFavoriteError::ServerError + })?; + + let user_session = match maybe_user_session { + Some(session) => session, + None => { + return Err(DeleteFavoriteError::NotAuthorized); + } + }; + + let mut maybe_delete_as = None; + + if request.as_mod.unwrap_or(false) && user_session.can_ban_users { + // 1) Delete as moderator + maybe_delete_as = Some(DeleteFavoriteAs::Moderator); + } else { + let favorite = get_favorite(&path.favorite_token, &mut mysql_connection) + .await + .map_err(|err| { + error!("error with query: {:?}", err); + DeleteFavoriteError::ServerError + })? + .ok_or(DeleteFavoriteError::NotFound)?; + + // 2) Delete as author + if favorite.user_token == user_session.user_token_typed { + maybe_delete_as = Some(DeleteFavoriteAs::Author); + } + + // 3) Delete as object owner + if maybe_delete_as.is_none() { + // TODO: Search for owner of the entity. + } + + // 4) Last ditch - try to see if they're a moderator again. + if maybe_delete_as.is_none() && user_session.can_ban_users { + maybe_delete_as = Some(DeleteFavoriteAs::Moderator); + } + } + + let delete_as = match maybe_delete_as { + Some(delete_as) => delete_as, + None => return Err(DeleteFavoriteError::NotAuthorized), + }; + + let query_result = delete_favorite( + &path.favorite_token, + delete_as, + &mut mysql_connection + ).await; + + match query_result { + Ok(_) => {}, + Err(err) => { + warn!("Update tts mod approval status DB error: {:?}", err); + return Err(DeleteFavoriteError::ServerError); + } + }; + + Ok(simple_json_success()) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_handler.rs new file mode 100644 index 0000000000..31e7d45573 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_handler.rs @@ -0,0 +1,160 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use std::fmt; +use std::sync::Arc; + +use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Path; +use chrono::{DateTime, Utc}; +use log::warn; + +use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; +use mysql_queries::queries::favorites::favorite_entity_token::FavoriteEntityToken; +use mysql_queries::queries::favorites::list_favorites_for_entity::list_favorites_for_entity; +use tokens::tokens::favorites::FavoriteToken; +use tokens::tokens::users::UserToken; + +use crate::http_server::common_responses::user_details_lite::{DefaultAvatarInfo, UserDetailsLight}; +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::server_state::ServerState; +use crate::user_avatars::default_avatar_color_from_username::default_avatar_color_from_username; +use crate::user_avatars::default_avatar_from_username::default_avatar_from_username; + +/// For the URL PathInfo +#[derive(Deserialize)] +pub struct ListFavoritesPathInfo { + entity_type: FavoriteEntityType, + entity_token: String, +} + +#[derive(Serialize)] +pub struct ListFavoritesSuccessResponse { + pub success: bool, + pub favorites: Vec, +} + +#[derive(Serialize)] +pub struct Favorite { + pub token: FavoriteToken, + + pub user: UserDetailsLight, + + //pub mod_fields: FavoriteForListModFields, + + pub created_at: DateTime, + pub updated_at: DateTime, + + #[deprecated(note="switch to UserDetailsLight")] + pub user_token: UserToken, + + #[deprecated(note="switch to UserDetailsLight")] + pub username: String, + + #[deprecated(note="switch to UserDetailsLight")] + pub user_display_name: String, + + #[deprecated(note="switch to UserDetailsLight")] + pub user_gravatar_hash: String, + + #[deprecated(note="switch to UserDetailsLight")] + pub default_avatar_index: u8, + + #[deprecated(note="switch to UserDetailsLight")] + pub default_avatar_color_index: u8, +} + +// TODO +//pub struct FavoriteForListModFields { +// pub creator_ip_address: String, +// pub editor_ip_address: String, +// pub maybe_user_deleted_at: Option>, +// pub maybe_mod_deleted_at: Option>, +// pub maybe_object_owner_deleted_at: Option>, +//} + +#[derive(Debug)] +pub enum ListFavoritesError { + ServerError, +} + +impl ResponseError for ListFavoritesError { + fn status_code(&self) -> StatusCode { + match *self { + ListFavoritesError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + ListFavoritesError::ServerError => "server error".to_string(), + }; + + to_simple_json_error(&error_reason, self.status_code()) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for ListFavoritesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +pub async fn list_favorites_handler( + _http_request: HttpRequest, + path: Path, + server_state: web::Data> +) -> Result +{ + let entity_token = FavoriteEntityToken::from_entity_type_and_token( + path.entity_type, &path.entity_token); + + let query_results = list_favorites_for_entity( + entity_token, + &server_state.mysql_pool, + ).await; + + let favorites = match query_results { + Ok(results) => results, + Err(e) => { + warn!("Query error: {:?}", e); + return Err(ListFavoritesError::ServerError); + } + }; + + let response = ListFavoritesSuccessResponse { + success: true, + favorites: favorites.into_iter() + .map(|favorite| Favorite { + token: favorite.token, + user: UserDetailsLight { + user_token: favorite.user_token.clone(), + username: favorite.username.to_string(), // NB: Cloned because of ref use for avatar below + display_name: favorite.user_display_name.clone(), + gravatar_hash: favorite.user_gravatar_hash.clone(), + default_avatar: DefaultAvatarInfo::from_username(&favorite.username), + }, + user_token: favorite.user_token, + username: favorite.username.to_string(), // NB: Cloned because of ref use for avatar below + user_display_name: favorite.user_display_name, + user_gravatar_hash: favorite.user_gravatar_hash, + default_avatar_index: default_avatar_from_username(&favorite.username), + default_avatar_color_index: default_avatar_color_from_username(&favorite.username), + created_at: favorite.created_at, + updated_at: favorite.updated_at, + }) + .collect(), + }; + + let body = serde_json::to_string(&response) + .map_err(|_e| ListFavoritesError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/mod.rs new file mode 100644 index 0000000000..bf67e09771 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/mod.rs @@ -0,0 +1,3 @@ +pub mod create_favorite_handler; +pub mod delete_favorite_handler; +pub mod list_favorites_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs index c862f9055e..81af5ee4b2 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs @@ -25,3 +25,4 @@ pub mod voice_clone_requests; pub mod voice_conversion; pub mod voice_designer; pub mod w2l; +pub mod favorites; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs index 870977024e..d833b6b902 100644 --- a/crates/service/web/storyteller_web/src/routes.rs +++ b/crates/service/web/storyteller_web/src/routes.rs @@ -29,6 +29,9 @@ use crate::http_server::endpoints::comments::list_comments_handler::list_comment use crate::http_server::endpoints::download_job::enqueue_generic_download::enqueue_generic_download_handler; use crate::http_server::endpoints::download_job::get_generic_upload_job_status::get_generic_download_job_status_handler; use crate::http_server::endpoints::events::list_events::list_events_handler; +use crate::http_server::endpoints::favorites::create_favorite_handler::create_favorite_handler; +use crate::http_server::endpoints::favorites::delete_favorite_handler::delete_favorite_handler; +use crate::http_server::endpoints::favorites::list_favorites_handler::list_favorites_handler; use crate::http_server::endpoints::flags::design_refresh_flag::disable_design_refresh_flag_handler::disable_design_refresh_flag_handler; use crate::http_server::endpoints::flags::design_refresh_flag::enable_design_refresh_flag_handler::enable_design_refresh_flag_handler; use crate::http_server::endpoints::inference_job::get_inference_job_status::get_inference_job_status_handler; @@ -186,6 +189,14 @@ pub fn add_routes (app: App, server_environment: ServerEnvironment) -> .add_post("/v1/comments/delete/{comment_token}", delete_comment_handler) .into_app(); + // ==================== Favorites ==================== + + let mut app = RouteBuilder::from_app(app) + .add_get("/v1/favorites/list/{entity_type}/{entity_token}", list_favorites_handler) + .add_post("/v1/favorites/new", create_favorite_handler) + .add_post("/v1/favorites/delete/{favorite_token}", delete_favorite_handler) + .into_app(); + // ==================== Animations ==================== let mut app = RouteBuilder::from_app(app) From b2bdf16c1ccdfc884cc693a1f427eda9e09486fe Mon Sep 17 00:00:00 2001 From: kasisnu <5044120+kasisnu@users.noreply.github.com> Date: Thu, 12 Oct 2023 22:29:58 +0530 Subject: [PATCH 2/5] update postman collection w/favoritesc --- .../2023-11-17-094126_favorite/up.sql | 4 - ...Service Collection.postman_collection.json | 75 ++++++++++++++++++- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/_sql/migrations/2023-11-17-094126_favorite/up.sql b/_sql/migrations/2023-11-17-094126_favorite/up.sql index 429c8bacf4..9f1f22e5b6 100644 --- a/_sql/migrations/2023-11-17-094126_favorite/up.sql +++ b/_sql/migrations/2023-11-17-094126_favorite/up.sql @@ -25,10 +25,6 @@ CREATE TABLE favorites -- The token of the entity the favorite applies to entity_token VARCHAR(32) NOT NULL, - -- For abuse tracking. - -- Wide enough for IPv4/6 - creator_ip_address VARCHAR(40) NOT NULL, - -- ========== VECTOR CLOCK ========== -- Incremented with every update. diff --git a/crates/service/web/storyteller_web/src/postman_collections/Base Service Collection.postman_collection.json b/crates/service/web/storyteller_web/src/postman_collections/Base Service Collection.postman_collection.json index cd118356fe..dc5132bbff 100644 --- a/crates/service/web/storyteller_web/src/postman_collections/Base Service Collection.postman_collection.json +++ b/crates/service/web/storyteller_web/src/postman_collections/Base Service Collection.postman_collection.json @@ -1,9 +1,9 @@ { "info": { - "_postman_id": "febcb082-e71c-46c4-a172-adf736e01298", + "_postman_id": "5b9aab26-f591-4264-b75b-3075dbeaf6df", "name": "Base Service Collection", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "30159615" + "_exporter_id": "10633600" }, "item": [ { @@ -84,6 +84,77 @@ } ] }, + { + "name": "Favorites", + "item": [ + { + "name": "List Favorites", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/v1/favorites/list/user/U:0XTEZ7HX29B9C", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "v1", + "favorites", + "list", + "user", + "U:0XTEZ7HX29B9C" + ] + } + }, + "response": [] + }, + { + "name": "Create Favorite", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{baseUrl}}/v1/comments/new", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "v1", + "comments", + "new" + ] + } + }, + "response": [] + }, + { + "name": "Delete Favorite", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{baseUrl}}/v1/favorites/delete/:favorite_token", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "v1", + "favorites", + "delete", + ":favorite_token" + ], + "variable": [ + { + "key": "favorite_token", + "value": "" + } + ] + } + }, + "response": [] + } + ] + }, { "name": "Animations", "item": [ From 2614ba5eb9321c3f8f9a208f6b366a151b4ed988 Mon Sep 17 00:00:00 2001 From: kasisnu <5044120+kasisnu@users.noreply.github.com> Date: Fri, 17 Nov 2023 12:51:48 +0530 Subject: [PATCH 3/5] remove ip address fields, and be able to favorite media files --- .../enums/src/by_table/favorites/favorite_entity_type.rs | 9 +++++++++ .../src/queries/favorites/favorite_entity_token.rs | 5 ++++- .../mysql_queries/src/queries/favorites/get_favorite.rs | 4 ---- .../src/queries/favorites/insert_favorite.rs | 6 +----- .../src/queries/favorites/list_favorites_for_entity.rs | 6 ------ .../endpoints/favorites/create_favorite_handler.rs | 4 ---- 6 files changed, 14 insertions(+), 20 deletions(-) diff --git a/crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs b/crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs index 67f1b88e13..595ea68c66 100644 --- a/crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs +++ b/crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs @@ -28,6 +28,10 @@ pub enum FavoriteEntityType { /// W2L result #[serde(rename = "w2l_result")] W2lResult, + + /// MediaFile + #[serde(rename = "media_file")] + MediaFile, } // TODO(bt, 2023-01-17): This desperately needs MySQL integration tests! @@ -43,6 +47,7 @@ impl FavoriteEntityType { Self::TtsResult => "tts_result", Self::W2lTemplate => "w2l_template", Self::W2lResult => "w2l_result", + Self::MediaFile => "media_file", } } @@ -53,6 +58,7 @@ impl FavoriteEntityType { "tts_result" => Ok(Self::TtsResult), "w2l_template" => Ok(Self::W2lTemplate), "w2l_result" => Ok(Self::W2lResult), + "media_file" => Ok(Self::MediaFile), _ => Err(format!("invalid value: {:?}", value)), } } @@ -73,6 +79,7 @@ mod tests { assert_serialization(FavoriteEntityType::TtsResult, "tts_result"); assert_serialization(FavoriteEntityType::W2lTemplate, "w2l_template"); assert_serialization(FavoriteEntityType::W2lResult, "w2l_result"); + assert_serialization(FavoriteEntityType::MediaFile, "media_file"); } } @@ -86,6 +93,7 @@ mod tests { assert_eq!(FavoriteEntityType::TtsResult.to_str(), "tts_result"); assert_eq!(FavoriteEntityType::W2lTemplate.to_str(), "w2l_template"); assert_eq!(FavoriteEntityType::W2lResult.to_str(), "w2l_result"); + assert_eq!(FavoriteEntityType::MediaFile.to_str(), "media_file"); } #[test] @@ -95,6 +103,7 @@ mod tests { assert_eq!(FavoriteEntityType::from_str("tts_result").unwrap(), FavoriteEntityType::TtsResult); assert_eq!(FavoriteEntityType::from_str("w2l_template").unwrap(), FavoriteEntityType::W2lTemplate); assert_eq!(FavoriteEntityType::from_str("w2l_result").unwrap(), FavoriteEntityType::W2lResult); + assert_eq!(FavoriteEntityType::from_str("media_file").unwrap(), FavoriteEntityType::MediaFile); assert!(FavoriteEntityType::from_str("foo").is_err()); } } diff --git a/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs b/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs index f62fb91cef..306971c17e 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs @@ -1,4 +1,5 @@ use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; +use tokens::tokens::media_files::MediaFileToken; use tokens::tokens::tts_models::TtsModelToken; use tokens::tokens::w2l_templates::W2lTemplateToken; use tokens::tokens::users::UserToken; @@ -9,10 +10,10 @@ pub enum FavoriteEntityToken { TtsResult(String), // TODO: Strong type W2lTemplate(W2lTemplateToken), W2lResult(String), // TODO: Strong type + MediaFile(MediaFileToken), } impl FavoriteEntityToken { - pub fn from_entity_type_and_token(entity_type: FavoriteEntityType, token: &str) -> Self { match entity_type { FavoriteEntityType::User => Self::User(UserToken::new_from_str(token)), @@ -20,6 +21,7 @@ impl FavoriteEntityToken { FavoriteEntityType::TtsResult => Self::TtsResult(token.to_string()), FavoriteEntityType::W2lTemplate => Self::W2lTemplate(W2lTemplateToken::new_from_str(token)), FavoriteEntityType::W2lResult => Self::W2lResult(token.to_string()), + FavoriteEntityType::MediaFile => Self::MediaFile(MediaFileToken::new_from_str(token)), } } @@ -30,6 +32,7 @@ impl FavoriteEntityToken { FavoriteEntityToken::TtsResult(tts_result_token) => (FavoriteEntityType::TtsResult, tts_result_token.as_str()), FavoriteEntityToken::W2lTemplate(w2l_template_token) => (FavoriteEntityType::W2lTemplate, w2l_template_token.as_str()), FavoriteEntityToken::W2lResult(w2l_result_token) => (FavoriteEntityType::W2lResult, w2l_result_token.as_str()), + FavoriteEntityToken::MediaFile(media_file_token) => (FavoriteEntityType::MediaFile, media_file_token.as_str()), }; (entity_type, entity_token) } diff --git a/crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs b/crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs index 9360aa8fa7..b6ad512834 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs @@ -25,7 +25,6 @@ pub struct Favorite { } pub struct FavoriteForListModFields { - pub creator_ip_address: String, pub maybe_user_deleted_at: Option>, pub maybe_mod_deleted_at: Option>, } @@ -52,7 +51,6 @@ SELECT u.display_name as user_display_name, u.email_gravatar_hash as user_gravatar_hash, - f.creator_ip_address, f.created_at, f.updated_at, f.user_deleted_at, @@ -80,7 +78,6 @@ WHERE user_display_name: favorite.user_display_name, user_gravatar_hash: favorite.user_gravatar_hash, mod_fields: FavoriteForListModFields { - creator_ip_address: favorite.creator_ip_address, maybe_user_deleted_at: favorite.user_deleted_at, maybe_mod_deleted_at: favorite.mod_deleted_at, }, @@ -105,7 +102,6 @@ pub struct RawFavorite { pub user_display_name: String, pub user_gravatar_hash: String, - pub creator_ip_address: String, pub created_at: DateTime, pub updated_at: DateTime, diff --git a/crates/lib/mysql_queries/src/queries/favorites/insert_favorite.rs b/crates/lib/mysql_queries/src/queries/favorites/insert_favorite.rs index 8e12c2dffe..9138d71cc9 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/insert_favorite.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/insert_favorite.rs @@ -18,8 +18,6 @@ pub struct InsertFavoriteArgs<'e, 'c, E> pub user_token: &'e UserToken, - pub creator_ip_address: &'e str, - pub mysql_executor: E, // TODO: Not sure if this works to tell the compiler we need the lifetime annotation. @@ -45,15 +43,13 @@ SET uuid_idempotency_token = ?, user_token = ?, entity_type = ?, - entity_token = ?, - creator_ip_address = ? + entity_token = ? "#, &favorite_token, args.uuid_idempotency_token, args.user_token, entity_type, entity_token, - args.creator_ip_address, ) .execute(args.mysql_executor) .await; diff --git a/crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs b/crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs index a0d77aa3f1..e062bfafbc 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs @@ -24,7 +24,6 @@ pub struct FavoriteForList { } pub struct FavoriteForListModFields { - pub creator_ip_address: String, pub maybe_user_deleted_at: Option>, pub maybe_mod_deleted_at: Option>, } @@ -46,7 +45,6 @@ SELECT u.display_name as user_display_name, u.email_gravatar_hash as user_gravatar_hash, - f.creator_ip_address, f.created_at, f.updated_at, @@ -87,7 +85,6 @@ LIMIT 50 user_display_name: favorite.user_display_name, user_gravatar_hash: favorite.user_gravatar_hash, mod_fields: FavoriteForListModFields { - creator_ip_address: favorite.creator_ip_address, maybe_user_deleted_at: favorite.user_deleted_at, maybe_mod_deleted_at: favorite.mod_deleted_at, }, @@ -106,9 +103,6 @@ pub struct RawFavoriteForList { pub user_display_name: String, pub user_gravatar_hash: String, - - pub creator_ip_address: String, - pub created_at: DateTime, pub updated_at: DateTime, pub user_deleted_at: Option>, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs index 399aa8e570..8fd52804e7 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs @@ -12,7 +12,6 @@ use actix_web::http::StatusCode; use log::warn; use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; -use http_server_common::request::get_request_ip::get_request_ip; use mysql_queries::queries::favorites::favorite_entity_token::FavoriteEntityToken; use mysql_queries::queries::favorites::insert_favorite::{insert_favorite, InsertFavoriteArgs}; use tokens::tokens::favorites::FavoriteToken; @@ -97,8 +96,6 @@ pub async fn create_favorite_handler( } }; - let ip_address = get_request_ip(&http_request); - let entity_token = FavoriteEntityToken::from_entity_type_and_token( request.entity_type, &request.entity_token); @@ -106,7 +103,6 @@ pub async fn create_favorite_handler( entity_token: &entity_token, uuid_idempotency_token: &request.uuid_idempotency_token, user_token: &user_session.user_token_typed, - creator_ip_address: &ip_address, mysql_executor: &mut mysql_connection, phantom: Default::default(), }).await; From 18bd819da2271b96d1720b52b6e9c86e3ebc1efe Mon Sep 17 00:00:00 2001 From: kasisnu <5044120+kasisnu@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:38:17 +0530 Subject: [PATCH 4/5] zsvoice & VC models can be favorited --- .../2023-11-17-094126_favorite/up.sql | 5 ----- .../by_table/favorites/favorite_entity_type.rs | 18 ++++++++++++++++++ .../{insert_favorite.rs => create_favorite.rs} | 16 ++++++++-------- .../queries/favorites/favorite_entity_token.rs | 8 ++++++++ .../mysql_queries/src/queries/favorites/mod.rs | 2 +- .../favorites/create_favorite_handler.rs | 6 ++---- 6 files changed, 37 insertions(+), 18 deletions(-) rename crates/lib/mysql_queries/src/queries/favorites/{insert_favorite.rs => create_favorite.rs} (83%) diff --git a/_sql/migrations/2023-11-17-094126_favorite/up.sql b/_sql/migrations/2023-11-17-094126_favorite/up.sql index 9f1f22e5b6..9da17dcfe4 100644 --- a/_sql/migrations/2023-11-17-094126_favorite/up.sql +++ b/_sql/migrations/2023-11-17-094126_favorite/up.sql @@ -12,10 +12,6 @@ CREATE TABLE favorites -- Effective "primary key" (PUBLIC) token VARCHAR(32) NOT NULL, - -- Idempotency token from client - -- This is so the frontend client doesn't submit duplicate items. - uuid_idempotency_token VARCHAR(36) NOT NULL, - -- User making the favorite user_token VARCHAR(32) NOT NULL, @@ -43,7 +39,6 @@ CREATE TABLE favorites -- INDICES -- PRIMARY KEY (id), UNIQUE KEY (token), - UNIQUE KEY (uuid_idempotency_token), KEY index_entity_type_entity_token (entity_type, entity_token), KEY index_user_token (user_token), KEY index_created_at (created_at), diff --git a/crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs b/crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs index 595ea68c66..5b96603ff1 100644 --- a/crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs +++ b/crates/_types/enums/src/by_table/favorites/favorite_entity_type.rs @@ -32,6 +32,14 @@ pub enum FavoriteEntityType { /// MediaFile #[serde(rename = "media_file")] MediaFile, + + /// VoiceConversionModel + #[serde(rename = "voice_conversion_model")] + VoiceConversionModel, + + /// ZsVoice + #[serde(rename = "zs_voice")] + ZsVoice, } // TODO(bt, 2023-01-17): This desperately needs MySQL integration tests! @@ -48,6 +56,8 @@ impl FavoriteEntityType { Self::W2lTemplate => "w2l_template", Self::W2lResult => "w2l_result", Self::MediaFile => "media_file", + Self::VoiceConversionModel => "voice_conversion_model", + Self::ZsVoice => "zs_voice", } } @@ -59,6 +69,8 @@ impl FavoriteEntityType { "w2l_template" => Ok(Self::W2lTemplate), "w2l_result" => Ok(Self::W2lResult), "media_file" => Ok(Self::MediaFile), + "voice_conversion_model" => Ok(Self::VoiceConversionModel), + "zs_voice" => Ok(Self::ZsVoice), _ => Err(format!("invalid value: {:?}", value)), } } @@ -80,6 +92,8 @@ mod tests { assert_serialization(FavoriteEntityType::W2lTemplate, "w2l_template"); assert_serialization(FavoriteEntityType::W2lResult, "w2l_result"); assert_serialization(FavoriteEntityType::MediaFile, "media_file"); + assert_serialization(FavoriteEntityType::VoiceConversionModel, "voice_conversion_model"); + assert_serialization(FavoriteEntityType::ZsVoice, "zs_voice"); } } @@ -94,6 +108,8 @@ mod tests { assert_eq!(FavoriteEntityType::W2lTemplate.to_str(), "w2l_template"); assert_eq!(FavoriteEntityType::W2lResult.to_str(), "w2l_result"); assert_eq!(FavoriteEntityType::MediaFile.to_str(), "media_file"); + assert_eq!(FavoriteEntityType::VoiceConversionModel.to_str(), "voice_conversion_model"); + assert_eq!(FavoriteEntityType::ZsVoice.to_str(), "zs_voice"); } #[test] @@ -104,6 +120,8 @@ mod tests { assert_eq!(FavoriteEntityType::from_str("w2l_template").unwrap(), FavoriteEntityType::W2lTemplate); assert_eq!(FavoriteEntityType::from_str("w2l_result").unwrap(), FavoriteEntityType::W2lResult); assert_eq!(FavoriteEntityType::from_str("media_file").unwrap(), FavoriteEntityType::MediaFile); + assert_eq!(FavoriteEntityType::from_str("voice_conversion_model").unwrap(), FavoriteEntityType::VoiceConversionModel); + assert_eq!(FavoriteEntityType::from_str("zs_voice").unwrap(), FavoriteEntityType::ZsVoice); assert!(FavoriteEntityType::from_str("foo").is_err()); } } diff --git a/crates/lib/mysql_queries/src/queries/favorites/insert_favorite.rs b/crates/lib/mysql_queries/src/queries/favorites/create_favorite.rs similarity index 83% rename from crates/lib/mysql_queries/src/queries/favorites/insert_favorite.rs rename to crates/lib/mysql_queries/src/queries/favorites/create_favorite.rs index 9138d71cc9..4ab6a4b49a 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/insert_favorite.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/create_favorite.rs @@ -9,13 +9,11 @@ use tokens::tokens::users::UserToken; use crate::queries::favorites::favorite_entity_token::FavoriteEntityToken; -pub struct InsertFavoriteArgs<'e, 'c, E> +pub struct CreateFavoriteArgs<'e, 'c, E> where E: 'e + Executor<'c, Database = MySql> { pub entity_token: &'e FavoriteEntityToken, - pub uuid_idempotency_token: &'e str, - pub user_token: &'e UserToken, pub mysql_executor: E, @@ -25,10 +23,10 @@ pub struct InsertFavoriteArgs<'e, 'c, E> pub phantom: PhantomData<&'c E>, } -pub async fn insert_favorite<'e, 'c : 'e, E>( - args: InsertFavoriteArgs<'e, 'c, E>, +pub async fn create_favorite<'e, 'c : 'e, E>( + args: CreateFavoriteArgs<'e, 'c, E>, ) - -> AnyhowResult + -> AnyhowResult where E: 'e + Executor<'c, Database = MySql> { @@ -40,13 +38,15 @@ pub async fn insert_favorite<'e, 'c : 'e, E>( INSERT INTO favorites SET token = ?, - uuid_idempotency_token = ?, user_token = ?, entity_type = ?, entity_token = ? + +ON DUPLICATE KEY UPDATE + user_deleted_at = NULL, + version = version + 1 "#, &favorite_token, - args.uuid_idempotency_token, args.user_token, entity_type, entity_token, diff --git a/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs b/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs index 306971c17e..df6e8f952f 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs @@ -3,6 +3,8 @@ use tokens::tokens::media_files::MediaFileToken; use tokens::tokens::tts_models::TtsModelToken; use tokens::tokens::w2l_templates::W2lTemplateToken; use tokens::tokens::users::UserToken; +use tokens::tokens::voice_conversion_models::VoiceConversionModelToken; +use tokens::tokens::zs_voices::ZsVoiceToken; pub enum FavoriteEntityToken { User(UserToken), @@ -11,6 +13,8 @@ pub enum FavoriteEntityToken { W2lTemplate(W2lTemplateToken), W2lResult(String), // TODO: Strong type MediaFile(MediaFileToken), + VoiceConversionModel(VoiceConversionModelToken), + ZsVoice(ZsVoiceToken), } impl FavoriteEntityToken { @@ -22,6 +26,8 @@ impl FavoriteEntityToken { FavoriteEntityType::W2lTemplate => Self::W2lTemplate(W2lTemplateToken::new_from_str(token)), FavoriteEntityType::W2lResult => Self::W2lResult(token.to_string()), FavoriteEntityType::MediaFile => Self::MediaFile(MediaFileToken::new_from_str(token)), + FavoriteEntityType::VoiceConversionModel => Self::VoiceConversionModel(VoiceConversionModelToken::new_from_str(token)), + FavoriteEntityType::ZsVoice => Self::ZsVoice(ZsVoiceToken::new_from_str(token)), } } @@ -33,6 +39,8 @@ impl FavoriteEntityToken { FavoriteEntityToken::W2lTemplate(w2l_template_token) => (FavoriteEntityType::W2lTemplate, w2l_template_token.as_str()), FavoriteEntityToken::W2lResult(w2l_result_token) => (FavoriteEntityType::W2lResult, w2l_result_token.as_str()), FavoriteEntityToken::MediaFile(media_file_token) => (FavoriteEntityType::MediaFile, media_file_token.as_str()), + FavoriteEntityToken::VoiceConversionModel(voice_conversion_model_token) => (FavoriteEntityType::VoiceConversionModel, voice_conversion_model_token.as_str()), + FavoriteEntityToken::ZsVoice(zs_voice_token) => (FavoriteEntityType::ZsVoice, zs_voice_token.as_str()), }; (entity_type, entity_token) } diff --git a/crates/lib/mysql_queries/src/queries/favorites/mod.rs b/crates/lib/mysql_queries/src/queries/favorites/mod.rs index 7143c80f04..db92f0093d 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/mod.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/mod.rs @@ -1,5 +1,5 @@ pub mod favorite_entity_token; pub mod delete_favorite; pub mod get_favorite; -pub mod insert_favorite; +pub mod create_favorite; pub mod list_favorites_for_entity; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs index 8fd52804e7..abfc46ea32 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs @@ -13,14 +13,13 @@ use log::warn; use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; use mysql_queries::queries::favorites::favorite_entity_token::FavoriteEntityToken; -use mysql_queries::queries::favorites::insert_favorite::{insert_favorite, InsertFavoriteArgs}; +use mysql_queries::queries::favorites::create_favorite::{create_favorite, CreateFavoriteArgs}; use tokens::tokens::favorites::FavoriteToken; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; #[derive(Deserialize)] pub struct CreateFavoriteRequest { - uuid_idempotency_token: String, entity_token: String, entity_type: FavoriteEntityType, } @@ -99,9 +98,8 @@ pub async fn create_favorite_handler( let entity_token = FavoriteEntityToken::from_entity_type_and_token( request.entity_type, &request.entity_token); - let query_result = insert_favorite(InsertFavoriteArgs { + let query_result = create_favorite(CreateFavoriteArgs { entity_token: &entity_token, - uuid_idempotency_token: &request.uuid_idempotency_token, user_token: &user_session.user_token_typed, mysql_executor: &mut mysql_connection, phantom: Default::default(), From 2c502359a4848517716d6f45c69a5ed121838943 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 21 Nov 2023 15:11:31 -0500 Subject: [PATCH 5/5] user favorites, entity favorites, etc. + minor changes --- .../down.sql | 0 .../up.sql | 16 +- .../src/queries/favorites/create_favorite.rs | 2 +- .../src/queries/favorites/delete_favorite.rs | 50 ++---- .../favorites/favorite_entity_token.rs | 19 +-- .../src/queries/favorites/get_favorite.rs | 20 +-- .../favorites/list_favorites_for_entity.rs | 71 ++++----- .../queries/favorites/list_user_favorites.rs | 147 ++++++++++++++++++ .../favorites/list_user_favorites_result.rs | 85 ++++++++++ .../src/queries/favorites/mod.rs | 10 +- .../favorites/create_favorite_handler.rs | 10 +- .../favorites/delete_favorite_handler.rs | 46 +----- ...s => list_favorites_for_entity_handler.rs} | 40 +---- .../list_favorites_for_session_handler.rs | 141 +++++++++++++++++ .../list_favorites_for_user_handler.rs | 138 ++++++++++++++++ .../http_server/endpoints/favorites/mod.rs | 4 +- .../service/web/storyteller_web/src/routes.rs | 11 +- 17 files changed, 602 insertions(+), 208 deletions(-) rename _sql/migrations/{2023-11-17-094126_favorite => 2023-11-17-094126_favorites}/down.sql (100%) rename _sql/migrations/{2023-11-17-094126_favorite => 2023-11-17-094126_favorites}/up.sql (69%) create mode 100644 crates/lib/mysql_queries/src/queries/favorites/list_user_favorites.rs create mode 100644 crates/lib/mysql_queries/src/queries/favorites/list_user_favorites_result.rs rename crates/service/web/storyteller_web/src/http_server/endpoints/favorites/{list_favorites_handler.rs => list_favorites_for_entity_handler.rs} (69%) create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_for_session_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_for_user_handler.rs diff --git a/_sql/migrations/2023-11-17-094126_favorite/down.sql b/_sql/migrations/2023-11-17-094126_favorites/down.sql similarity index 100% rename from _sql/migrations/2023-11-17-094126_favorite/down.sql rename to _sql/migrations/2023-11-17-094126_favorites/down.sql diff --git a/_sql/migrations/2023-11-17-094126_favorite/up.sql b/_sql/migrations/2023-11-17-094126_favorites/up.sql similarity index 69% rename from _sql/migrations/2023-11-17-094126_favorite/up.sql rename to _sql/migrations/2023-11-17-094126_favorites/up.sql index 9da17dcfe4..f819a241eb 100644 --- a/_sql/migrations/2023-11-17-094126_favorite/up.sql +++ b/_sql/migrations/2023-11-17-094126_favorites/up.sql @@ -24,26 +24,22 @@ CREATE TABLE favorites -- ========== VECTOR CLOCK ========== -- Incremented with every update. - version INT NOT NULL DEFAULT 0, + version INT NOT NULL DEFAULT 0, -- ========== TIMESTAMPS ========== - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - - -- If this is removed by a mod. - -- It completely disappears from the system. - user_deleted_at TIMESTAMP NULL, - mod_deleted_at TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, -- INDICES -- PRIMARY KEY (id), UNIQUE KEY (token), KEY index_entity_type_entity_token (entity_type, entity_token), + KEY index_user_token_entity_token (user_token, entity_type), KEY index_user_token (user_token), KEY index_created_at (created_at), - KEY index_user_deleted_at (user_deleted_at), - KEY index_mod_deleted_at (mod_deleted_at) + KEY index_user_deleted_at (deleted_at) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 diff --git a/crates/lib/mysql_queries/src/queries/favorites/create_favorite.rs b/crates/lib/mysql_queries/src/queries/favorites/create_favorite.rs index 4ab6a4b49a..a6decf5fff 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/create_favorite.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/create_favorite.rs @@ -43,7 +43,7 @@ SET entity_token = ? ON DUPLICATE KEY UPDATE - user_deleted_at = NULL, + deleted_at = NULL, version = version + 1 "#, &favorite_token, diff --git a/crates/lib/mysql_queries/src/queries/favorites/delete_favorite.rs b/crates/lib/mysql_queries/src/queries/favorites/delete_favorite.rs index 3d0df0b3b0..0ec20c4e6a 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/delete_favorite.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/delete_favorite.rs @@ -2,54 +2,34 @@ use sqlx::{Executor, MySql}; use errors::AnyhowResult; use tokens::tokens::favorites::FavoriteToken; +use tokens::tokens::users::UserToken; -#[derive(Copy, Clone)] -pub enum DeleteFavoriteAs { - Author, - Moderator, -} - +// NB: UserToken is only supplied as an optimistic form of validation pub async fn delete_favorite<'e, 'c, E>( favorite_token: &'e FavoriteToken, - delete_as: DeleteFavoriteAs, + user_token: &'e UserToken, mysql_executor: E ) -> AnyhowResult<()> where E: 'e + Executor<'c, Database = MySql> { - match delete_as { - DeleteFavoriteAs::Author => { - sqlx::query!( - r#" + sqlx::query!( + r#" UPDATE favorites SET - user_deleted_at = CURRENT_TIMESTAMP, - version = version + 1 +deleted_at = CURRENT_TIMESTAMP, +version = version + 1 WHERE - token = ? +token = ? +AND user_token = ? LIMIT 1 - "#, - favorite_token - ) - } - DeleteFavoriteAs::Moderator => { - sqlx::query!( - r#" -UPDATE favorites -SET - mod_deleted_at = CURRENT_TIMESTAMP, - version = version + 1 -WHERE - token = ? -LIMIT 1 - "#, - favorite_token - ) - } - } - .execute(mysql_executor) - .await?; + "#, + favorite_token, + user_token + ) + .execute(mysql_executor) + .await?; Ok(()) } diff --git a/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs b/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs index df6e8f952f..3a0007253f 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/favorite_entity_token.rs @@ -1,17 +1,19 @@ use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; use tokens::tokens::media_files::MediaFileToken; use tokens::tokens::tts_models::TtsModelToken; -use tokens::tokens::w2l_templates::W2lTemplateToken; +use tokens::tokens::tts_results::TtsResultToken; use tokens::tokens::users::UserToken; use tokens::tokens::voice_conversion_models::VoiceConversionModelToken; +use tokens::tokens::w2l_results::W2lResultToken; +use tokens::tokens::w2l_templates::W2lTemplateToken; use tokens::tokens::zs_voices::ZsVoiceToken; pub enum FavoriteEntityToken { User(UserToken), TtsModel(TtsModelToken), - TtsResult(String), // TODO: Strong type + TtsResult(TtsResultToken), W2lTemplate(W2lTemplateToken), - W2lResult(String), // TODO: Strong type + W2lResult(W2lResultToken), MediaFile(MediaFileToken), VoiceConversionModel(VoiceConversionModelToken), ZsVoice(ZsVoiceToken), @@ -22,9 +24,9 @@ impl FavoriteEntityToken { match entity_type { FavoriteEntityType::User => Self::User(UserToken::new_from_str(token)), FavoriteEntityType::TtsModel => Self::TtsModel(TtsModelToken::new_from_str(token)), - FavoriteEntityType::TtsResult => Self::TtsResult(token.to_string()), + FavoriteEntityType::TtsResult => Self::TtsResult(TtsResultToken::new_from_str(token)), FavoriteEntityType::W2lTemplate => Self::W2lTemplate(W2lTemplateToken::new_from_str(token)), - FavoriteEntityType::W2lResult => Self::W2lResult(token.to_string()), + FavoriteEntityType::W2lResult => Self::W2lResult(W2lResultToken::new_from_str(token)), FavoriteEntityType::MediaFile => Self::MediaFile(MediaFileToken::new_from_str(token)), FavoriteEntityType::VoiceConversionModel => Self::VoiceConversionModel(VoiceConversionModelToken::new_from_str(token)), FavoriteEntityType::ZsVoice => Self::ZsVoice(ZsVoiceToken::new_from_str(token)), @@ -32,7 +34,7 @@ impl FavoriteEntityToken { } pub fn get_composite_keys(&self) -> (FavoriteEntityType, &str) { - let (entity_type, entity_token) = match self { + match self { FavoriteEntityToken::User(user_token) => (FavoriteEntityType::User, user_token.as_str()), FavoriteEntityToken::TtsModel(tts_model_token) => (FavoriteEntityType::TtsModel, tts_model_token.as_str()), FavoriteEntityToken::TtsResult(tts_result_token) => (FavoriteEntityType::TtsResult, tts_result_token.as_str()), @@ -40,8 +42,7 @@ impl FavoriteEntityToken { FavoriteEntityToken::W2lResult(w2l_result_token) => (FavoriteEntityType::W2lResult, w2l_result_token.as_str()), FavoriteEntityToken::MediaFile(media_file_token) => (FavoriteEntityType::MediaFile, media_file_token.as_str()), FavoriteEntityToken::VoiceConversionModel(voice_conversion_model_token) => (FavoriteEntityType::VoiceConversionModel, voice_conversion_model_token.as_str()), - FavoriteEntityToken::ZsVoice(zs_voice_token) => (FavoriteEntityType::ZsVoice, zs_voice_token.as_str()), - }; - (entity_type, entity_token) + FavoriteEntityToken::ZsVoice(zs_voice_token) => (FavoriteEntityType::ZsVoice, zs_voice_token.as_str()), + } } } diff --git a/crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs b/crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs index b6ad512834..01d46404bc 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/get_favorite.rs @@ -18,15 +18,9 @@ pub struct Favorite { pub user_display_name: String, pub user_gravatar_hash: String, - pub mod_fields: FavoriteForListModFields, - pub created_at: DateTime, pub updated_at: DateTime, -} - -pub struct FavoriteForListModFields { - pub maybe_user_deleted_at: Option>, - pub maybe_mod_deleted_at: Option>, + pub maybe_deleted_at: Option>, } pub async fn get_favorite<'e, 'c, E>( @@ -53,8 +47,7 @@ SELECT f.created_at, f.updated_at, - f.user_deleted_at, - f.mod_deleted_at + f.deleted_at FROM favorites AS f @@ -77,12 +70,9 @@ WHERE username: favorite.username, user_display_name: favorite.user_display_name, user_gravatar_hash: favorite.user_gravatar_hash, - mod_fields: FavoriteForListModFields { - maybe_user_deleted_at: favorite.user_deleted_at, - maybe_mod_deleted_at: favorite.mod_deleted_at, - }, created_at: favorite.created_at, updated_at: favorite.updated_at, + maybe_deleted_at: favorite.deleted_at, })), Err(err) => match err { sqlx::Error::RowNotFound => Ok(None), @@ -102,9 +92,7 @@ pub struct RawFavorite { pub user_display_name: String, pub user_gravatar_hash: String, - pub created_at: DateTime, pub updated_at: DateTime, - pub user_deleted_at: Option>, - pub mod_deleted_at: Option>, + pub deleted_at: Option>, } diff --git a/crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs b/crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs index e062bfafbc..e1080ac694 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/list_favorites_for_entity.rs @@ -9,7 +9,7 @@ use tokens::tokens::users::UserToken; use crate::queries::favorites::favorite_entity_token::FavoriteEntityToken; -pub struct FavoriteForList { +pub struct Favorite { pub token: FavoriteToken, pub user_token: UserToken, @@ -17,26 +17,20 @@ pub struct FavoriteForList { pub user_display_name: String, pub user_gravatar_hash: String, - pub mod_fields: FavoriteForListModFields, - pub created_at: DateTime, pub updated_at: DateTime, -} - -pub struct FavoriteForListModFields { - pub maybe_user_deleted_at: Option>, - pub maybe_mod_deleted_at: Option>, + pub maybe_deleted_at: Option>, } pub async fn list_favorites_for_entity( favorite_entity_token: FavoriteEntityToken, mysql_pool: &MySqlPool -) -> AnyhowResult> { +) -> AnyhowResult> { let (entity_type, entity_token) = favorite_entity_token.get_composite_keys(); let maybe_results= sqlx::query_as!( - RawFavoriteForList, + RawFavoriteRecord, r#" SELECT f.token as `token: tokens::tokens::favorites::FavoriteToken`, @@ -45,11 +39,9 @@ SELECT u.display_name as user_display_name, u.email_gravatar_hash as user_gravatar_hash, - f.created_at, f.updated_at, - f.user_deleted_at, - f.mod_deleted_at + f.deleted_at FROM favorites AS f @@ -58,8 +50,7 @@ JOIN users AS u WHERE f.entity_type = ? AND f.entity_token = ? - AND f.user_deleted_at IS NULL - AND f.mod_deleted_at IS NULL + AND f.deleted_at IS NULL ORDER BY f.id DESC LIMIT 50 "#, @@ -73,38 +64,40 @@ LIMIT 50 Err(err) => match err { sqlx::Error::RowNotFound => Ok(Vec::new()), _ => { - warn!("list ip bans db error: {:?}", err); + warn!("list favorites db error: {:?}", err); Err(anyhow!("error with query: {:?}", err)) } }, Ok(results) => Ok(results.into_iter() - .map(|favorite| FavoriteForList { - token: favorite.token, - user_token: favorite.user_token, - username: favorite.username, - user_display_name: favorite.user_display_name, - user_gravatar_hash: favorite.user_gravatar_hash, - mod_fields: FavoriteForListModFields { - maybe_user_deleted_at: favorite.user_deleted_at, - maybe_mod_deleted_at: favorite.mod_deleted_at, - }, - created_at: favorite.created_at, - updated_at: favorite.updated_at, - }) + .map(|favorite| favorite.into_public_type()) .collect()), } } -pub struct RawFavoriteForList { - pub token: FavoriteToken, +pub struct RawFavoriteRecord { + token: FavoriteToken, - pub user_token: UserToken, - pub username: String, - pub user_display_name: String, - pub user_gravatar_hash: String, + user_token: UserToken, + username: String, + user_display_name: String, + user_gravatar_hash: String, - pub created_at: DateTime, - pub updated_at: DateTime, - pub user_deleted_at: Option>, - pub mod_deleted_at: Option>, + created_at: DateTime, + updated_at: DateTime, + deleted_at: Option>, +} + +impl RawFavoriteRecord { + pub fn into_public_type(self) -> Favorite { + Favorite { + token: self.token, + user_token: self.user_token, + username: self.username, + user_display_name: self.user_display_name, + user_gravatar_hash: self.user_gravatar_hash, + created_at: self.created_at, + updated_at: self.updated_at, + maybe_deleted_at: self.deleted_at, + } + } } diff --git a/crates/lib/mysql_queries/src/queries/favorites/list_user_favorites.rs b/crates/lib/mysql_queries/src/queries/favorites/list_user_favorites.rs new file mode 100644 index 0000000000..501677846f --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/favorites/list_user_favorites.rs @@ -0,0 +1,147 @@ +use anyhow::anyhow; +use log::warn; +use sqlx::MySqlPool; +use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; + +use errors::AnyhowResult; +use tokens::tokens::users::UserToken; + +use crate::queries::favorites::favorite_entity_token::FavoriteEntityToken; +use crate::queries::favorites::list_user_favorites_result::{UserFavorite, RawUserFavoriteRecord}; + +pub async fn list_user_favorites( + username: &str, + mysql_pool: &MySqlPool +) -> AnyhowResult> { + + /// TODO(bt,2023-11-21): Maybe this query can use a switch + /// See: https://stackoverflow.com/questions/2761574/mysql-use-case-else-value-as-join-parameter + let maybe_results= sqlx::query_as!( + RawUserFavoriteRecord, + r#" +SELECT + f.token as `token: tokens::tokens::favorites::FavoriteToken`, + f.entity_type as `entity_type: enums::by_table::favorites::favorite_entity_type::FavoriteEntityType`, + f.entity_token, + f.user_token as `user_token: tokens::tokens::users::UserToken`, + u.username, + u.display_name as user_display_name, + u.email_gravatar_hash as user_gravatar_hash, + f.created_at, + f.updated_at, + f.deleted_at, + + media_files.media_type as `maybe_media_file_type: enums::by_table::media_files::media_file_type::MediaFileType`, + media_files.origin_product_category as `maybe_media_file_origin_category: enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory`, + + tts_models.title as maybe_descriptive_text_tts_model_title, + tts_results.raw_inference_text as maybe_descriptive_text_tts_result_inference_text, + users.display_name as maybe_descriptive_text_user_display_name, + voice_conversion_models.title as maybe_descriptive_text_voice_conversion_model_title, + zs_voices.title as maybe_descriptive_text_zs_voice_title + +FROM + favorites AS f +JOIN users AS u + ON f.user_token = u.token + +LEFT OUTER JOIN media_files ON media_files.token = f.entity_token +LEFT OUTER JOIN tts_models ON tts_models.token = f.entity_token +LEFT OUTER JOIN tts_results ON tts_results.token = f.entity_token +LEFT OUTER JOIN users ON users.token = f.entity_token +LEFT OUTER JOIN voice_conversion_models ON voice_conversion_models.token = f.entity_token +LEFT OUTER JOIN zs_voices ON zs_voices.token = f.entity_token + +WHERE + u.username = ? + AND f.deleted_at IS NULL +ORDER BY f.id DESC +LIMIT 50 + "#, + username + ) + .fetch_all(mysql_pool) + .await; + + match maybe_results { + Err(err) => match err { + sqlx::Error::RowNotFound => Ok(Vec::new()), + _ => { + warn!("list favorites db error: {:?}", err); + Err(anyhow!("error with query: {:?}", err)) + } + }, + Ok(results) => Ok(results.into_iter() + .map(|favorite| favorite.into_public_type()) + .collect()), + } +} + +pub async fn list_user_favorites_by_entity_type( + username: &str, + entity_type: FavoriteEntityType, + mysql_pool: &MySqlPool +) -> AnyhowResult> { + + let maybe_results= sqlx::query_as!( + RawUserFavoriteRecord, + r#" +SELECT + f.token as `token: tokens::tokens::favorites::FavoriteToken`, + f.entity_type as `entity_type: enums::by_table::favorites::favorite_entity_type::FavoriteEntityType`, + f.entity_token, + f.user_token as `user_token: tokens::tokens::users::UserToken`, + u.username, + u.display_name as user_display_name, + u.email_gravatar_hash as user_gravatar_hash, + f.created_at, + f.updated_at, + f.deleted_at, + + media_files.media_type as `maybe_media_file_type: enums::by_table::media_files::media_file_type::MediaFileType`, + media_files.origin_product_category as `maybe_media_file_origin_category: enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory`, + + tts_models.title as maybe_descriptive_text_tts_model_title, + tts_results.raw_inference_text as maybe_descriptive_text_tts_result_inference_text, + users.display_name as maybe_descriptive_text_user_display_name, + voice_conversion_models.title as maybe_descriptive_text_voice_conversion_model_title, + zs_voices.title as maybe_descriptive_text_zs_voice_title + +FROM + favorites AS f +JOIN users AS u + ON f.user_token = u.token + +LEFT OUTER JOIN media_files ON media_files.token = f.entity_token +LEFT OUTER JOIN tts_models ON tts_models.token = f.entity_token +LEFT OUTER JOIN tts_results ON tts_results.token = f.entity_token +LEFT OUTER JOIN users ON users.token = f.entity_token +LEFT OUTER JOIN voice_conversion_models ON voice_conversion_models.token = f.entity_token +LEFT OUTER JOIN zs_voices ON zs_voices.token = f.entity_token + +WHERE + u.username = ? + AND f.entity_type = ? + AND f.deleted_at IS NULL +ORDER BY f.id DESC +LIMIT 50 + "#, + username, + entity_type + ) + .fetch_all(mysql_pool) + .await; + + match maybe_results { + Err(err) => match err { + sqlx::Error::RowNotFound => Ok(Vec::new()), + _ => { + warn!("list favorites db error: {:?}", err); + Err(anyhow!("error with query: {:?}", err)) + } + }, + Ok(results) => Ok(results.into_iter() + .map(|favorite| favorite.into_public_type()) + .collect()), + } +} diff --git a/crates/lib/mysql_queries/src/queries/favorites/list_user_favorites_result.rs b/crates/lib/mysql_queries/src/queries/favorites/list_user_favorites_result.rs new file mode 100644 index 0000000000..c391a5d8e6 --- /dev/null +++ b/crates/lib/mysql_queries/src/queries/favorites/list_user_favorites_result.rs @@ -0,0 +1,85 @@ +use chrono::{DateTime, Utc}; + +use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; +use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory; +use enums::by_table::media_files::media_file_type::MediaFileType; +use tokens::tokens::favorites::FavoriteToken; +use tokens::tokens::users::UserToken; + +pub struct UserFavorite { + pub token: FavoriteToken, + + pub entity_type: FavoriteEntityType, + pub entity_token: String, + + /// Something descriptive about the favorited entity. + /// This might be TTS text, a username, etc. It depends on the entity type. + pub maybe_entity_descriptive_text: Option, + + pub user_token: UserToken, + pub username: String, + pub user_display_name: String, + pub user_gravatar_hash: String, + + pub created_at: DateTime, + pub updated_at: DateTime, + pub maybe_deleted_at: Option>, +} + +pub struct RawUserFavoriteRecord { + pub (crate) token: FavoriteToken, + + pub (crate) entity_type: FavoriteEntityType, + pub (crate) entity_token: String, + + pub (crate) user_token: UserToken, + pub (crate) username: String, + pub (crate) user_display_name: String, + pub (crate) user_gravatar_hash: String, + + pub (crate) created_at: DateTime, + pub (crate) updated_at: DateTime, + pub (crate) deleted_at: Option>, + + pub (crate) maybe_media_file_type: Option, + pub (crate) maybe_media_file_origin_category: Option, + + pub (crate) maybe_descriptive_text_tts_model_title: Option, + pub (crate) maybe_descriptive_text_tts_result_inference_text: Option, + pub (crate) maybe_descriptive_text_user_display_name: Option, + pub (crate) maybe_descriptive_text_voice_conversion_model_title: Option, + pub (crate) maybe_descriptive_text_zs_voice_title: Option, +} + +impl RawUserFavoriteRecord { + pub fn into_public_type(self) -> UserFavorite { + UserFavorite { + token: self.token, + entity_type: self.entity_type, + entity_token: self.entity_token, + maybe_entity_descriptive_text: match self.entity_type { + FavoriteEntityType::User => self.maybe_descriptive_text_user_display_name, + FavoriteEntityType::TtsModel => self.maybe_descriptive_text_tts_model_title, + FavoriteEntityType::TtsResult => self.maybe_descriptive_text_tts_result_inference_text, + FavoriteEntityType::W2lTemplate => None, + FavoriteEntityType::W2lResult => None, + // TODO(bt,2023-11-21): Summary text needs to be better enriched. + FavoriteEntityType::MediaFile => self.maybe_media_file_type + .and_then(|media_file_type| match media_file_type { + MediaFileType::Audio => Some("audio media file".to_string()), + MediaFileType::Image => Some("image media file".to_string()), + MediaFileType::Video => Some("video media file".to_string()), + }), + FavoriteEntityType::VoiceConversionModel => self.maybe_descriptive_text_voice_conversion_model_title, + FavoriteEntityType::ZsVoice => self.maybe_descriptive_text_zs_voice_title, + }, + user_token: self.user_token, + username: self.username, + user_display_name: self.user_display_name, + user_gravatar_hash: self.user_gravatar_hash, + created_at: self.created_at, + updated_at: self.updated_at, + maybe_deleted_at: self.deleted_at, + } + } +} diff --git a/crates/lib/mysql_queries/src/queries/favorites/mod.rs b/crates/lib/mysql_queries/src/queries/favorites/mod.rs index db92f0093d..8cc8a3c7c5 100644 --- a/crates/lib/mysql_queries/src/queries/favorites/mod.rs +++ b/crates/lib/mysql_queries/src/queries/favorites/mod.rs @@ -1,5 +1,7 @@ -pub mod favorite_entity_token; -pub mod delete_favorite; -pub mod get_favorite; pub mod create_favorite; -pub mod list_favorites_for_entity; \ No newline at end of file +pub mod delete_favorite; +pub mod favorite_entity_token; +pub mod get_favorite; +pub mod list_favorites_for_entity; +pub mod list_user_favorites; +pub mod list_user_favorites_result; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs index abfc46ea32..cb4aa5f5f4 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/create_favorite_handler.rs @@ -101,7 +101,7 @@ pub async fn create_favorite_handler( let query_result = create_favorite(CreateFavoriteArgs { entity_token: &entity_token, user_token: &user_session.user_token_typed, - mysql_executor: &mut mysql_connection, + mysql_executor: &mut *mysql_connection, phantom: Default::default(), }).await; @@ -113,14 +113,6 @@ pub async fn create_favorite_handler( } }; - // server_state.firehose_publisher.publish_favorite_created( - // &user_session.user_token_typed, &favorite_token) - // .await - // .map_err(|e| { - // warn!("error publishing event: {:?}", e); - // CreateFavoriteError::ServerError - // })?; - let response = CreateFavoriteSuccessResponse { success: true, favorite_token, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/delete_favorite_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/delete_favorite_handler.rs index 90e24f4941..be28d24534 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/delete_favorite_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/delete_favorite_handler.rs @@ -10,10 +10,9 @@ use actix_web::{HttpRequest, HttpResponse, web}; use actix_web::error::ResponseError; use actix_web::http::StatusCode; use actix_web::web::Path; -use log::{error, warn}; +use log::warn; -use mysql_queries::queries::favorites::delete_favorite::{delete_favorite, DeleteFavoriteAs}; -use mysql_queries::queries::favorites::get_favorite::get_favorite; +use mysql_queries::queries::favorites::delete_favorite::delete_favorite; use tokens::tokens::favorites::FavoriteToken; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; @@ -72,7 +71,7 @@ impl fmt::Display for DeleteFavoriteError { pub async fn delete_favorite_handler( http_request: HttpRequest, path: Path, - request: web::Json, + _request: web::Json, server_state: web::Data> ) -> Result { let mut mysql_connection = server_state.mysql_pool @@ -99,45 +98,10 @@ pub async fn delete_favorite_handler( } }; - let mut maybe_delete_as = None; - - if request.as_mod.unwrap_or(false) && user_session.can_ban_users { - // 1) Delete as moderator - maybe_delete_as = Some(DeleteFavoriteAs::Moderator); - } else { - let favorite = get_favorite(&path.favorite_token, &mut mysql_connection) - .await - .map_err(|err| { - error!("error with query: {:?}", err); - DeleteFavoriteError::ServerError - })? - .ok_or(DeleteFavoriteError::NotFound)?; - - // 2) Delete as author - if favorite.user_token == user_session.user_token_typed { - maybe_delete_as = Some(DeleteFavoriteAs::Author); - } - - // 3) Delete as object owner - if maybe_delete_as.is_none() { - // TODO: Search for owner of the entity. - } - - // 4) Last ditch - try to see if they're a moderator again. - if maybe_delete_as.is_none() && user_session.can_ban_users { - maybe_delete_as = Some(DeleteFavoriteAs::Moderator); - } - } - - let delete_as = match maybe_delete_as { - Some(delete_as) => delete_as, - None => return Err(DeleteFavoriteError::NotAuthorized), - }; - let query_result = delete_favorite( &path.favorite_token, - delete_as, - &mut mysql_connection + &user_session.user_token_typed, + &mut *mysql_connection ).await; match query_result { diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_for_entity_handler.rs similarity index 69% rename from crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_for_entity_handler.rs index 31e7d45573..efbb773b84 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_for_entity_handler.rs @@ -17,13 +17,10 @@ use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; use mysql_queries::queries::favorites::favorite_entity_token::FavoriteEntityToken; use mysql_queries::queries::favorites::list_favorites_for_entity::list_favorites_for_entity; use tokens::tokens::favorites::FavoriteToken; -use tokens::tokens::users::UserToken; use crate::http_server::common_responses::user_details_lite::{DefaultAvatarInfo, UserDetailsLight}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use crate::user_avatars::default_avatar_color_from_username::default_avatar_color_from_username; -use crate::user_avatars::default_avatar_from_username::default_avatar_from_username; /// For the URL PathInfo #[derive(Deserialize)] @@ -44,39 +41,10 @@ pub struct Favorite { pub user: UserDetailsLight, - //pub mod_fields: FavoriteForListModFields, - pub created_at: DateTime, pub updated_at: DateTime, - - #[deprecated(note="switch to UserDetailsLight")] - pub user_token: UserToken, - - #[deprecated(note="switch to UserDetailsLight")] - pub username: String, - - #[deprecated(note="switch to UserDetailsLight")] - pub user_display_name: String, - - #[deprecated(note="switch to UserDetailsLight")] - pub user_gravatar_hash: String, - - #[deprecated(note="switch to UserDetailsLight")] - pub default_avatar_index: u8, - - #[deprecated(note="switch to UserDetailsLight")] - pub default_avatar_color_index: u8, } -// TODO -//pub struct FavoriteForListModFields { -// pub creator_ip_address: String, -// pub editor_ip_address: String, -// pub maybe_user_deleted_at: Option>, -// pub maybe_mod_deleted_at: Option>, -// pub maybe_object_owner_deleted_at: Option>, -//} - #[derive(Debug)] pub enum ListFavoritesError { ServerError, @@ -105,7 +73,7 @@ impl fmt::Display for ListFavoritesError { } } -pub async fn list_favorites_handler( +pub async fn list_favorites_for_entity_handler( _http_request: HttpRequest, path: Path, server_state: web::Data> @@ -139,12 +107,6 @@ pub async fn list_favorites_handler( gravatar_hash: favorite.user_gravatar_hash.clone(), default_avatar: DefaultAvatarInfo::from_username(&favorite.username), }, - user_token: favorite.user_token, - username: favorite.username.to_string(), // NB: Cloned because of ref use for avatar below - user_display_name: favorite.user_display_name, - user_gravatar_hash: favorite.user_gravatar_hash, - default_avatar_index: default_avatar_from_username(&favorite.username), - default_avatar_color_index: default_avatar_color_from_username(&favorite.username), created_at: favorite.created_at, updated_at: favorite.updated_at, }) diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_for_session_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_for_session_handler.rs new file mode 100644 index 0000000000..cdc11f8550 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_for_session_handler.rs @@ -0,0 +1,141 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use std::fmt; +use std::sync::Arc; + +use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Query; +use chrono::{DateTime, Utc}; +use log::warn; + +use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; +use mysql_queries::queries::favorites::list_user_favorites::{list_user_favorites, list_user_favorites_by_entity_type}; +use tokens::tokens::favorites::FavoriteToken; + +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::server_state::ServerState; + +#[derive(Deserialize)] +pub struct ListFavoritesQueryData { + maybe_scoped_entity_type: Option, +} + +#[derive(Serialize)] +pub struct ListFavoritesSuccessResponse { + pub success: bool, + pub favorites: Vec, +} + +#[derive(Serialize)] +pub struct Favorite { + pub token: FavoriteToken, + + pub details: FavoriteDetails, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Serialize)] +pub struct FavoriteDetails { + // TODO: This needs titles or some other summary metadata. + pub entity_type: FavoriteEntityType, + pub entity_token: String, + + // TODO: Populate this for TTS + pub maybe_summary_text: Option, + + // TODO: Populate this for images, video, etc. + pub maybe_thumbnail_url: Option, +} + +#[derive(Debug)] +pub enum ListFavoritesError { + ServerError, + NotAuthorizedError, +} + +impl ResponseError for ListFavoritesError { + fn status_code(&self) -> StatusCode { + match *self { + ListFavoritesError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + ListFavoritesError::NotAuthorizedError => StatusCode::UNAUTHORIZED, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + ListFavoritesError::ServerError => "server error".to_string(), + ListFavoritesError::NotAuthorizedError => "not authorized".to_string(), + }; + + to_simple_json_error(&error_reason, self.status_code()) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for ListFavoritesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +pub async fn list_favorites_for_session_handler( + http_request: HttpRequest, + query: Query, + server_state: web::Data> +) -> Result +{ + let user_session = server_state.session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + ListFavoritesError::ServerError + })? + .ok_or(ListFavoritesError::NotAuthorizedError)?; + + let query_results = match query.maybe_scoped_entity_type { + None => list_user_favorites(&user_session.username, &server_state.mysql_pool).await, + Some(entity_type) => + list_user_favorites_by_entity_type(&user_session.username, entity_type, &server_state.mysql_pool) + .await, + }; + + let favorites = match query_results { + Ok(results) => results, + Err(e) => { + warn!("Query error: {:?}", e); + return Err(ListFavoritesError::ServerError); + } + }; + + let response = ListFavoritesSuccessResponse { + success: true, + favorites: favorites.into_iter() + .map(|favorite| Favorite { + token: favorite.token, + details: FavoriteDetails { + entity_type: favorite.entity_type, + entity_token: favorite.entity_token, + maybe_summary_text: None, + maybe_thumbnail_url: None, + }, + created_at: favorite.created_at, + updated_at: favorite.updated_at, + }) + .collect(), + }; + + let body = serde_json::to_string(&response) + .map_err(|_e| ListFavoritesError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_for_user_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_for_user_handler.rs new file mode 100644 index 0000000000..071b1fcb0e --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/list_favorites_for_user_handler.rs @@ -0,0 +1,138 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use std::fmt; +use std::sync::Arc; + +use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::{Path, Query}; +use chrono::{DateTime, Utc}; +use log::warn; + +use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType; +use mysql_queries::queries::favorites::list_user_favorites::{list_user_favorites, list_user_favorites_by_entity_type}; +use tokens::tokens::favorites::FavoriteToken; + +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::server_state::ServerState; + +#[derive(Deserialize)] +pub struct ListFavoritesPathInfo { + username: String, +} + +#[derive(Deserialize)] +pub struct ListFavoritesQueryData { + maybe_scoped_entity_type: Option, +} + +#[derive(Serialize)] +pub struct ListFavoritesSuccessResponse { + pub success: bool, + pub favorites: Vec, +} + +#[derive(Serialize)] +pub struct Favorite { + pub token: FavoriteToken, + + pub details: FavoriteDetails, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Serialize)] +pub struct FavoriteDetails { + // TODO: This needs titles or some other summary metadata. + pub entity_type: FavoriteEntityType, + pub entity_token: String, + + // TODO: Populate this for TTS + pub maybe_summary_text: Option, + + // TODO: Populate this for images, video, etc. + pub maybe_thumbnail_url: Option, +} + +#[derive(Debug)] +pub enum ListFavoritesError { + ServerError, +} + +impl ResponseError for ListFavoritesError { + fn status_code(&self) -> StatusCode { + match *self { + ListFavoritesError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + ListFavoritesError::ServerError => "server error".to_string(), + }; + + to_simple_json_error(&error_reason, self.status_code()) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for ListFavoritesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +pub async fn list_favorites_for_user_handler( + _http_request: HttpRequest, + path: Path, + query: Query, + server_state: web::Data> +) -> Result +{ + + let query_results = match query.maybe_scoped_entity_type { + None => list_user_favorites(&path.username, &server_state.mysql_pool).await, + Some(entity_type) => + list_user_favorites_by_entity_type(&path.username, entity_type, &server_state.mysql_pool) + .await, + }; + + let favorites = match query_results { + Ok(results) => results, + Err(e) => { + warn!("Query error: {:?}", e); + return Err(ListFavoritesError::ServerError); + } + }; + + let response = ListFavoritesSuccessResponse { + success: true, + favorites: favorites.into_iter() + .map(|favorite| Favorite { + token: favorite.token, + details: FavoriteDetails { + entity_type: favorite.entity_type, + entity_token: favorite.entity_token, + maybe_summary_text: favorite.maybe_entity_descriptive_text, + // TODO(bt,2023-11-21): Thumbnails need proper support. We should build them as a + // first-class system before handling the backfill here. + maybe_thumbnail_url: None, + }, + created_at: favorite.created_at, + updated_at: favorite.updated_at, + }) + .collect(), + }; + + let body = serde_json::to_string(&response) + .map_err(|_e| ListFavoritesError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/mod.rs index bf67e09771..4529d2f18b 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/favorites/mod.rs @@ -1,3 +1,5 @@ pub mod create_favorite_handler; pub mod delete_favorite_handler; -pub mod list_favorites_handler; +pub mod list_favorites_for_entity_handler; +pub mod list_favorites_for_session_handler; +pub mod list_favorites_for_user_handler; diff --git a/crates/service/web/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs index d833b6b902..9b6f920061 100644 --- a/crates/service/web/storyteller_web/src/routes.rs +++ b/crates/service/web/storyteller_web/src/routes.rs @@ -3,7 +3,6 @@ use actix_service::ServiceFactory; use actix_web::{App, HttpResponse, web}; use actix_web::dev::{ServiceRequest, ServiceResponse}; use actix_web::error::Error; -use log::warn; use actix_helpers::route_builder::RouteBuilder; use billing_component::default_routes::add_suggested_stripe_billing_routes; @@ -31,7 +30,9 @@ use crate::http_server::endpoints::download_job::get_generic_upload_job_status:: use crate::http_server::endpoints::events::list_events::list_events_handler; use crate::http_server::endpoints::favorites::create_favorite_handler::create_favorite_handler; use crate::http_server::endpoints::favorites::delete_favorite_handler::delete_favorite_handler; -use crate::http_server::endpoints::favorites::list_favorites_handler::list_favorites_handler; +use crate::http_server::endpoints::favorites::list_favorites_for_entity_handler::list_favorites_for_entity_handler; +use crate::http_server::endpoints::favorites::list_favorites_for_session_handler::list_favorites_for_session_handler; +use crate::http_server::endpoints::favorites::list_favorites_for_user_handler::list_favorites_for_user_handler; use crate::http_server::endpoints::flags::design_refresh_flag::disable_design_refresh_flag_handler::disable_design_refresh_flag_handler; use crate::http_server::endpoints::flags::design_refresh_flag::enable_design_refresh_flag_handler::enable_design_refresh_flag_handler; use crate::http_server::endpoints::inference_job::get_inference_job_status::get_inference_job_status_handler; @@ -192,9 +193,11 @@ pub fn add_routes (app: App, server_environment: ServerEnvironment) -> // ==================== Favorites ==================== let mut app = RouteBuilder::from_app(app) - .add_get("/v1/favorites/list/{entity_type}/{entity_token}", list_favorites_handler) - .add_post("/v1/favorites/new", create_favorite_handler) + .add_post("/v1/favorites/create", create_favorite_handler) .add_post("/v1/favorites/delete/{favorite_token}", delete_favorite_handler) + .add_get("/v1/favorites/list/session", list_favorites_for_session_handler) + .add_get("/v1/favorites/list/user/{username}", list_favorites_for_user_handler) + .add_get("/v1/favorites/list/entity/{entity_type}/{entity_token}", list_favorites_for_entity_handler) .into_app(); // ==================== Animations ====================