mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
Merge pull request #95 from storytold/oct-12-mithoo
super basic favorites impl
This commit is contained in:
26 files changed
+1524
-4
No files matched your search
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
DROP TABLE favorites;
|
||||
@@ -0,0 +1,46 @@
|
||||
-- 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,
|
||||
|
||||
-- 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,
|
||||
|
||||
-- ========== 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,
|
||||
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 (deleted_at)
|
||||
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_bin;
|
||||
@@ -0,0 +1,128 @@
|
||||
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,
|
||||
|
||||
/// 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!
|
||||
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",
|
||||
Self::MediaFile => "media_file",
|
||||
Self::VoiceConversionModel => "voice_conversion_model",
|
||||
Self::ZsVoice => "zs_voice",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
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),
|
||||
"media_file" => Ok(Self::MediaFile),
|
||||
"voice_conversion_model" => Ok(Self::VoiceConversionModel),
|
||||
"zs_voice" => Ok(Self::ZsVoice),
|
||||
_ => 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");
|
||||
assert_serialization(FavoriteEntityType::MediaFile, "media_file");
|
||||
assert_serialization(FavoriteEntityType::VoiceConversionModel, "voice_conversion_model");
|
||||
assert_serialization(FavoriteEntityType::ZsVoice, "zs_voice");
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
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]
|
||||
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_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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod favorite_entity_type;
|
||||
@@ -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;
|
||||
@@ -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_",
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -0,0 +1,63 @@
|
||||
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 CreateFavoriteArgs<'e, 'c, E>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
pub entity_token: &'e FavoriteEntityToken,
|
||||
|
||||
pub user_token: &'e UserToken,
|
||||
|
||||
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 create_favorite<'e, 'c : 'e, E>(
|
||||
args: CreateFavoriteArgs<'e, 'c, E>,
|
||||
)
|
||||
-> AnyhowResult<FavoriteToken>
|
||||
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 = ?,
|
||||
user_token = ?,
|
||||
entity_type = ?,
|
||||
entity_token = ?
|
||||
|
||||
ON DUPLICATE KEY UPDATE
|
||||
deleted_at = NULL,
|
||||
version = version + 1
|
||||
"#,
|
||||
&favorite_token,
|
||||
args.user_token,
|
||||
entity_type,
|
||||
entity_token,
|
||||
)
|
||||
.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)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::favorites::FavoriteToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
// NB: UserToken is only supplied as an optimistic form of validation
|
||||
pub async fn delete_favorite<'e, 'c, E>(
|
||||
favorite_token: &'e FavoriteToken,
|
||||
user_token: &'e UserToken,
|
||||
mysql_executor: E
|
||||
)
|
||||
-> AnyhowResult<()>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE favorites
|
||||
SET
|
||||
deleted_at = CURRENT_TIMESTAMP,
|
||||
version = version + 1
|
||||
WHERE
|
||||
token = ?
|
||||
AND user_token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
favorite_token,
|
||||
user_token
|
||||
)
|
||||
.execute(mysql_executor)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use enums::by_table::favorites::favorite_entity_type::FavoriteEntityType;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::tts_models::TtsModelToken;
|
||||
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(TtsResultToken),
|
||||
W2lTemplate(W2lTemplateToken),
|
||||
W2lResult(W2lResultToken),
|
||||
MediaFile(MediaFileToken),
|
||||
VoiceConversionModel(VoiceConversionModelToken),
|
||||
ZsVoice(ZsVoiceToken),
|
||||
}
|
||||
|
||||
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(TtsResultToken::new_from_str(token)),
|
||||
FavoriteEntityType::W2lTemplate => Self::W2lTemplate(W2lTemplateToken::new_from_str(token)),
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_composite_keys(&self) -> (FavoriteEntityType, &str) {
|
||||
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()),
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
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 created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub maybe_deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub async fn get_favorite<'e, 'c, E>(
|
||||
favorite_token: &'e FavoriteToken,
|
||||
mysql_executor: E
|
||||
)
|
||||
-> AnyhowResult<Option<Favorite>>
|
||||
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.created_at,
|
||||
f.updated_at,
|
||||
f.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,
|
||||
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),
|
||||
_ => 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 created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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 Favorite {
|
||||
pub token: FavoriteToken,
|
||||
|
||||
pub user_token: UserToken,
|
||||
pub username: String,
|
||||
pub user_display_name: String,
|
||||
pub user_gravatar_hash: String,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub maybe_deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub async fn list_favorites_for_entity(
|
||||
favorite_entity_token: FavoriteEntityToken,
|
||||
mysql_pool: &MySqlPool
|
||||
) -> AnyhowResult<Vec<Favorite>> {
|
||||
|
||||
let (entity_type, entity_token) = favorite_entity_token.get_composite_keys();
|
||||
|
||||
let maybe_results= sqlx::query_as!(
|
||||
RawFavoriteRecord,
|
||||
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.created_at,
|
||||
f.updated_at,
|
||||
f.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.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 favorites db error: {:?}", err);
|
||||
Err(anyhow!("error with query: {:?}", err))
|
||||
}
|
||||
},
|
||||
Ok(results) => Ok(results.into_iter()
|
||||
.map(|favorite| favorite.into_public_type())
|
||||
.collect()),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RawFavoriteRecord {
|
||||
token: FavoriteToken,
|
||||
|
||||
user_token: UserToken,
|
||||
username: String,
|
||||
user_display_name: String,
|
||||
user_gravatar_hash: String,
|
||||
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Vec<UserFavorite>> {
|
||||
|
||||
/// 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<Vec<UserFavorite>> {
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
|
||||
pub user_token: UserToken,
|
||||
pub username: String,
|
||||
pub user_display_name: String,
|
||||
pub user_gravatar_hash: String,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub maybe_deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
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<Utc>,
|
||||
pub (crate) updated_at: DateTime<Utc>,
|
||||
pub (crate) deleted_at: Option<DateTime<Utc>>,
|
||||
|
||||
pub (crate) maybe_media_file_type: Option<MediaFileType>,
|
||||
pub (crate) maybe_media_file_origin_category: Option<MediaFileOriginCategory>,
|
||||
|
||||
pub (crate) maybe_descriptive_text_tts_model_title: Option<String>,
|
||||
pub (crate) maybe_descriptive_text_tts_result_inference_text: Option<String>,
|
||||
pub (crate) maybe_descriptive_text_user_display_name: Option<String>,
|
||||
pub (crate) maybe_descriptive_text_voice_conversion_model_title: Option<String>,
|
||||
pub (crate) maybe_descriptive_text_zs_voice_title: Option<String>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod create_favorite;
|
||||
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;
|
||||
@@ -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;
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
// 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 mysql_queries::queries::favorites::favorite_entity_token::FavoriteEntityToken;
|
||||
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 {
|
||||
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<CreateFavoriteRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<HttpResponse, CreateFavoriteError>
|
||||
{
|
||||
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 entity_token = FavoriteEntityToken::from_entity_type_and_token(
|
||||
request.entity_type, &request.entity_token);
|
||||
|
||||
let query_result = create_favorite(CreateFavoriteArgs {
|
||||
entity_token: &entity_token,
|
||||
user_token: &user_session.user_token_typed,
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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))
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// 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::warn;
|
||||
|
||||
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;
|
||||
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<bool>,
|
||||
}
|
||||
|
||||
#[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<DeleteFavoritePathInfo>,
|
||||
_request: web::Json<DeleteFavoriteRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, DeleteFavoriteError> {
|
||||
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 query_result = delete_favorite(
|
||||
&path.favorite_token,
|
||||
&user_session.user_token_typed,
|
||||
&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())
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
// 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 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;
|
||||
|
||||
/// 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<Favorite>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Favorite {
|
||||
pub token: FavoriteToken,
|
||||
|
||||
pub user: UserDetailsLight,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[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_entity_handler(
|
||||
_http_request: HttpRequest,
|
||||
path: Path<ListFavoritesPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, ListFavoritesError>
|
||||
{
|
||||
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),
|
||||
},
|
||||
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))
|
||||
}
|
||||
+141
@@ -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<FavoriteEntityType>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ListFavoritesSuccessResponse {
|
||||
pub success: bool,
|
||||
pub favorites: Vec<Favorite>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Favorite {
|
||||
pub token: FavoriteToken,
|
||||
|
||||
pub details: FavoriteDetails,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
|
||||
// TODO: Populate this for images, video, etc.
|
||||
pub maybe_thumbnail_url: Option<String>,
|
||||
}
|
||||
|
||||
#[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<ListFavoritesQueryData>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, ListFavoritesError>
|
||||
{
|
||||
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))
|
||||
}
|
||||
+138
@@ -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<FavoriteEntityType>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ListFavoritesSuccessResponse {
|
||||
pub success: bool,
|
||||
pub favorites: Vec<Favorite>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Favorite {
|
||||
pub token: FavoriteToken,
|
||||
|
||||
pub details: FavoriteDetails,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
|
||||
// TODO: Populate this for images, video, etc.
|
||||
pub maybe_thumbnail_url: Option<String>,
|
||||
}
|
||||
|
||||
#[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<ListFavoritesPathInfo>,
|
||||
query: Query<ListFavoritesQueryData>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, ListFavoritesError>
|
||||
{
|
||||
|
||||
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))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod create_favorite_handler;
|
||||
pub mod delete_favorite_handler;
|
||||
pub mod list_favorites_for_entity_handler;
|
||||
pub mod list_favorites_for_session_handler;
|
||||
pub mod list_favorites_for_user_handler;
|
||||
@@ -25,3 +25,4 @@ pub mod voice_clone_requests;
|
||||
pub mod voice_conversion;
|
||||
pub mod voice_designer;
|
||||
pub mod w2l;
|
||||
pub mod favorites;
|
||||
+73
-2
@@ -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": [
|
||||
|
||||
@@ -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;
|
||||
@@ -29,6 +28,11 @@ 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_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;
|
||||
@@ -186,6 +190,16 @@ pub fn add_routes<T, B> (app: App<T>, server_environment: ServerEnvironment) ->
|
||||
.add_post("/v1/comments/delete/{comment_token}", delete_comment_handler)
|
||||
.into_app();
|
||||
|
||||
// ==================== Favorites ====================
|
||||
|
||||
let mut app = RouteBuilder::from_app(app)
|
||||
.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 ====================
|
||||
|
||||
let mut app = RouteBuilder::from_app(app)
|
||||
|
||||
Reference in new issue
Block a user