Merge pull request #79 from storytold/oct-06-mithoo

adds list dataset handler and update/delete voices handler
This commit is contained in:
Brandon Thomas authored and GitHub committed 2023-10-09 20:58:25 -04:00
commit d4e8bfb1f8
18 files changed
+1171 -47

No files matched your search

@@ -13,7 +13,6 @@ pub struct CreateDatasetArgs<'a> {
pub maybe_creator_user_token: Option<&'a str>,
pub creator_ip_address: &'a str,
pub creator_set_visibility: &'a Visibility,
pub maybe_mod_user_token: Option<&'a str>,
pub mysql_pool: &'a MySqlPool
}
@@ -50,7 +49,6 @@ pub async fn create_dataset(args: CreateDatasetArgs<'_>) -> AnyhowResult<ZsDatas
maybe_creator_user_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
maybe_mod_user_token = ?,
maybe_creator_synthetic_id = ?
"#,
dataset_token.as_str(),
@@ -58,7 +56,6 @@ pub async fn create_dataset(args: CreateDatasetArgs<'_>) -> AnyhowResult<ZsDatas
args.maybe_creator_user_token,
args.creator_ip_address,
args.creator_set_visibility.to_str(),
args.maybe_mod_user_token,
maybe_creator_synthetic_id
).execute(args.mysql_pool).await;
// TODO(Kasisnu): This should probably rollback
@@ -1,13 +1,10 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use log::error;
use sqlx::{MySql, MySqlPool};
use sqlx::pool::PoolConnection;
use enums::by_table::media_uploads::media_upload_type::MediaUploadType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::files::media_upload::MediaUploadToken;
use tokens::tokens::zs_dataset::ZsDatasetToken;
@@ -17,6 +14,7 @@ pub struct ZsDataset {
pub ietf_language_tag: String,
pub ietf_primary_language_subtag: String,
pub maybe_creator_user_token: Option<String>,
pub creator_set_visibility: Visibility,
}
pub async fn get_dataset_by_token(
@@ -73,6 +71,7 @@ pub async fn get_dataset_by_token_with_connection(
ietf_language_tag: record.ietf_language_tag,
ietf_primary_language_subtag: record.ietf_primary_language_subtag,
maybe_creator_user_token: record.maybe_creator_user_token,
creator_set_visibility: record.creator_set_visibility,
}))
}
@@ -0,0 +1,150 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use log::{info, warn};
use sqlx::{MySql, MySqlPool};
use sqlx::pool::PoolConnection;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::zs_dataset::ZsDatasetToken;
// FIXME: This is the old style of query scoping and shouldn't be copied.
#[derive(Serialize)]
pub struct DatasetRecordForList {
pub dataset_token: String,
pub title: String,
pub creator_set_visibility: Visibility,
pub ietf_language_tag: String,
pub ietf_primary_language_subtag: String,
pub maybe_creator_user_token: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub async fn list_datasets_by_user_token(
mysql_pool: &MySqlPool,
user_token: &str,
can_see_deleted: bool,
is_owner: bool,
) -> AnyhowResult<Vec<DatasetRecordForList>> {
let mut connection = mysql_pool.acquire().await?;
list_datasets_with_connection(&mut connection, user_token, can_see_deleted, is_owner).await
}
pub async fn list_datasets_with_connection(
mysql_connection: &mut PoolConnection<MySql>,
user_token: &str,
can_see_deleted: bool,
is_owner: bool,
) -> AnyhowResult<Vec<DatasetRecordForList>> {
let datasets =
list_datasets_by_creator_token(mysql_connection, user_token, can_see_deleted)
.await;
let datasets : Vec<InternalRawDatasetRecordForList> = match datasets {
Ok(datasets) => datasets,
Err(err) => {
match err {
RowNotFound => {
return Ok(Vec::new());
},
_ => {
warn!("dataset list query error: {:?}", err);
return Err(anyhow!("dataset list query error"));
}
}
}
};
Ok(datasets.into_iter()
.map(|dataset| {
DatasetRecordForList{
dataset_token: dataset.token.to_string(),
title: dataset.title,
creator_set_visibility: dataset.creator_set_visibility,
ietf_language_tag: dataset.ietf_language_tag,
ietf_primary_language_subtag: dataset.ietf_primary_language_subtag,
maybe_creator_user_token: dataset.maybe_creator_user_token,
created_at: dataset.created_at,
updated_at: dataset.updated_at,
}
})
.filter(|dataset| {
is_owner || dataset.creator_set_visibility == Visibility::Public || can_see_deleted
})
.collect::<Vec<DatasetRecordForList>>())
}
async fn list_datasets_by_creator_token(
mysql_connection: &mut PoolConnection<MySql>,
creator_token: &str,
can_see_deleted: bool,
) -> AnyhowResult<Vec<InternalRawDatasetRecordForList>> {
// TODO: There has to be a better way.
// Sqlx doesn't like anything except string literals.
let maybe_datasets = if !can_see_deleted {
info!("listing datasets for user;");
sqlx::query_as!(
InternalRawDatasetRecordForList,
r#"
SELECT
zd.token as `token: tokens::tokens::zs_dataset::ZsDatasetToken`,
zd.title,
zd.ietf_language_tag,
zd.ietf_primary_language_subtag,
zd.maybe_creator_user_token,
zd.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
zd.created_at,
zd.updated_at
FROM zs_voice_datasets as zd
WHERE
zd.maybe_creator_user_token = ?
AND zd.user_deleted_at IS NULL
AND zd.mod_deleted_at IS NULL
"#,
creator_token)
.fetch_all(mysql_connection)
.await?
} else {
info!("listing datasets for user");
sqlx::query_as!(
InternalRawDatasetRecordForList
,
r#"
SELECT
zd.token as `token: tokens::tokens::zs_dataset::ZsDatasetToken`,
zd.title,
zd.ietf_language_tag,
zd.ietf_primary_language_subtag,
zd.maybe_creator_user_token,
zd.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
zd.created_at,
zd.updated_at
FROM zs_voice_datasets as zd
WHERE
zd.maybe_creator_user_token = ?
"#,
creator_token)
.fetch_all(mysql_connection)
.await?
};
Ok(maybe_datasets)
}
struct InternalRawDatasetRecordForList {
token: ZsDatasetToken,
title: String,
ietf_language_tag: String,
ietf_primary_language_subtag: String,
maybe_creator_user_token: Option<String>,
creator_set_visibility: Visibility,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
@@ -2,3 +2,4 @@ pub mod create_dataset;
pub mod delete_dataset;
pub mod get_dataset;
pub mod update_dataset;
pub mod list_datasets;
@@ -47,6 +47,8 @@ pub async fn update_dataset(args: UpdateDatasetArgs<'_>) -> AnyhowResult<()>{
creator_set_visibility = ?,
maybe_mod_user_token = ?,
maybe_creator_synthetic_id = ?,
ietf_language_tag = ?,
ietf_primary_language_subtag = ?,
version = version + 1
@@ -59,13 +61,15 @@ pub async fn update_dataset(args: UpdateDatasetArgs<'_>) -> AnyhowResult<()>{
args.creator_set_visibility.to_str(),
args.maybe_mod_user_token,
maybe_creator_synthetic_id,
args.ietf_language_tag,
args.ietf_primary_language_subtag,
args.dataset_token.as_str(),
).execute(args.mysql_pool).await;
// TODO(Kasisnu): This should probably rollback
transaction.commit().await?;
match query_result {
Ok(_) => Ok(()),
Err(err) => Err(anyhow!("zs dataset creation error: {:?}", err)),
Err(err) => Err(anyhow!("zs dataset update error: {:?}", err)),
}
}
@@ -1,3 +1,3 @@
pub mod datasets;
pub mod voice_samples;
pub mod voices;
pub mod voices;
@@ -0,0 +1,89 @@
use sqlx::MySqlPool;
use errors::AnyhowResult;
pub async fn delete_voice_as_user(
voice_token: &str,
mysql_pool: &MySqlPool
) -> AnyhowResult<()> {
let _r = sqlx::query!(
r#"
UPDATE zs_voices
SET
user_deleted_at = CURRENT_TIMESTAMP
WHERE
token = ?
LIMIT 1
"#,
voice_token,
)
.execute(mysql_pool)
.await?;
Ok(())
}
pub async fn delete_voice_as_mod(
voice_token: &str,
mod_user_token: &str,
mysql_pool: &MySqlPool
) -> AnyhowResult<()> {
let _r = sqlx::query!(
r#"
UPDATE zs_voices
SET
mod_deleted_at = CURRENT_TIMESTAMP,
maybe_mod_user_token = ?
WHERE
token = ?
LIMIT 1
"#,
mod_user_token,
voice_token,
)
.execute(mysql_pool)
.await?;
Ok(())
}
pub async fn undelete_voice_as_user(
voice_token: &str,
mysql_pool: &MySqlPool
) -> AnyhowResult<()> {
let _r = sqlx::query!(
r#"
UPDATE zs_voices
SET
user_deleted_at = NULL
WHERE
token = ?
LIMIT 1
"#,
voice_token,
)
.execute(mysql_pool)
.await?;
Ok(())
}
pub async fn undelete_voice_as_mod(
voice_token: &str,
mod_user_token: &str,
mysql_pool: &MySqlPool
) -> AnyhowResult<()> {
let _r = sqlx::query!(
r#"
UPDATE zs_voices
SET
mod_deleted_at = NULL,
maybe_mod_user_token = ?
WHERE
token = ?
LIMIT 1
"#,
mod_user_token,
voice_token,
)
.execute(mysql_pool)
.await?;
Ok(())
}
@@ -0,0 +1,132 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use log::error;
use sqlx::{MySql, MySqlPool};
use sqlx::pool::PoolConnection;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::zs_voice::ZsVoiceToken;
pub struct ZsVoice {
pub token: ZsVoiceToken,
pub title: String,
pub ietf_language_tag: String,
pub ietf_primary_language_subtag: String,
pub maybe_creator_user_token: Option<String>,
}
pub async fn get_voice_by_token(
voice_token: &ZsVoiceToken,
can_see_deleted: bool,
mysql_pool: &MySqlPool,
) -> AnyhowResult<Option<ZsVoice>> {
let mut connection = mysql_pool.acquire().await?;
get_voice_by_token_with_connection(
voice_token,
can_see_deleted,
&mut connection
).await
}
pub async fn get_voice_by_token_with_connection(
voice_token: &ZsVoiceToken,
can_see_deleted: bool,
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<Option<ZsVoice>> {
let maybe_result = if can_see_deleted {
select_include_deleted(
voice_token,
mysql_connection
).await
} else {
select_without_deleted(
voice_token,
mysql_connection
).await
};
let record = match maybe_result {
Ok(record) => record,
Err(sqlx::Error::RowNotFound) => {
return Ok(None);
},
Err(err) => {
error!(
"Error fetching voice by token: {:?}",
err
);
return Err(anyhow!(
"Error fetching voice by token: {:?}",
err
));
}
};
Ok(Some(ZsVoice {
token: record.token,
title: record.title,
ietf_language_tag: record.ietf_language_tag,
ietf_primary_language_subtag: record.ietf_primary_language_subtag,
maybe_creator_user_token: record.maybe_creator_user_token,
}))
}
async fn select_include_deleted(
voice_token: &ZsVoiceToken,
mysql_connection: &mut PoolConnection<MySql>,
) -> Result<RawVoice, sqlx::Error> {
sqlx::query_as!(
RawVoice,
r#"
SELECT
zv.token as `token: tokens::tokens::zs_voice::ZsVoiceToken`,
zv.title,
zv.ietf_language_tag,
zv.ietf_primary_language_subtag,
zv.maybe_creator_user_token,
zv.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`
FROM zs_voices as zv
WHERE
zv.token = ?
"#,
voice_token.as_str()
)
.fetch_one(mysql_connection).await
}
async fn select_without_deleted(
voice_token: &ZsVoiceToken,
mysql_connection: &mut PoolConnection<MySql>,
) -> Result<RawVoice, sqlx::Error> {
sqlx::query_as!(
RawVoice,
r#"
SELECT
zv.token as `token: tokens::tokens::zs_voice::ZsVoiceToken`,
zv.title,
zv.ietf_language_tag,
zv.ietf_primary_language_subtag,
zv.maybe_creator_user_token,
zv.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`
FROM zs_voices as zv
WHERE
zv.token = ?
AND zv.user_deleted_at IS NULL
AND zv.mod_deleted_at IS NULL
"#,
voice_token.as_str()
)
.fetch_one(mysql_connection).await
}
#[derive(Serialize)]
pub struct RawVoice {
token: ZsVoiceToken,
title: String,
ietf_language_tag: String,
ietf_primary_language_subtag: String,
maybe_creator_user_token: Option<String>,
creator_set_visibility: Visibility,
}
@@ -0,0 +1,149 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use log::{info, warn};
use sqlx::{MySql, MySqlPool};
use sqlx::pool::PoolConnection;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::zs_voice::ZsVoiceToken;
// FIXME: This is the old style of query scoping and shouldn't be copied.
#[derive(Serialize)]
pub struct VoiceRecordForList {
pub voice_token: String,
pub title: String,
pub creator_set_visibility: Visibility,
pub ietf_language_tag: String,
pub ietf_primary_language_subtag: String,
pub maybe_creator_user_token: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub async fn list_voices_by_user_token(
mysql_pool: &MySqlPool,
user_token: &str,
can_see_deleted: bool,
is_owner: bool,
) -> AnyhowResult<Vec<VoiceRecordForList>> {
let mut connection = mysql_pool.acquire().await?;
list_voices_with_connection(&mut connection, user_token, can_see_deleted, is_owner).await
}
pub async fn list_voices_with_connection(
mysql_connection: &mut PoolConnection<MySql>,
user_token: &str,
can_see_deleted: bool,
is_owner: bool,
) -> AnyhowResult<Vec<VoiceRecordForList>> {
let maybe_voices = list_dataset_by_creator(mysql_connection, user_token, can_see_deleted)
.await;
let voices : Vec<InternalRawVoiceRecordForList> = match maybe_voices {
Ok(voices) => voices,
Err(err) => {
match err {
RowNotFound => {
return Ok(Vec::new());
},
_ => {
warn!("voice list query error: {:?}", err);
return Err(anyhow!("voice list query error"));
}
}
}
};
Ok(voices.into_iter()
.map(|voice| {
VoiceRecordForList{
voice_token: voice.token.to_string(),
title: voice.title,
creator_set_visibility: voice.creator_set_visibility,
ietf_language_tag: voice.ietf_language_tag,
ietf_primary_language_subtag: voice.ietf_primary_language_subtag,
maybe_creator_user_token: voice.maybe_creator_user_token,
created_at: voice.created_at,
updated_at: voice.updated_at,
}
})
.filter(|dataset| {
is_owner || dataset.creator_set_visibility == Visibility::Public || can_see_deleted
})
.collect::<Vec<VoiceRecordForList>>())
}
async fn list_dataset_by_creator(
mysql_connection: &mut PoolConnection<MySql>,
creator_token: &str,
can_see_deleted: bool,
) -> AnyhowResult<Vec<InternalRawVoiceRecordForList>> {
// TODO: There has to be a better way.
// Sqlx doesn't like anything except string literals.
let maybe_voices = if !can_see_deleted {
info!("listing voices for user;");
sqlx::query_as!(
InternalRawVoiceRecordForList,
r#"
SELECT
zv.token as `token: tokens::tokens::zs_voice::ZsVoiceToken`,
zv.title,
zv.ietf_language_tag,
zv.ietf_primary_language_subtag,
zv.maybe_creator_user_token,
zv.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
zv.created_at,
zv.updated_at
FROM zs_voices as zv
WHERE
zv.maybe_creator_user_token = ?
AND zv.user_deleted_at IS NULL
AND zv.mod_deleted_at IS NULL
"#,
creator_token)
.fetch_all(mysql_connection)
.await?
} else {
info!("listing voices for user");
sqlx::query_as!(
InternalRawVoiceRecordForList
,
r#"
SELECT
zv.token as `token: tokens::tokens::zs_voice::ZsVoiceToken`,
zv.title,
zv.ietf_language_tag,
zv.ietf_primary_language_subtag,
zv.maybe_creator_user_token,
zv.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
zv.created_at,
zv.updated_at
FROM zs_voices as zv
WHERE
zv.maybe_creator_user_token = ?
"#,
creator_token)
.fetch_all(mysql_connection)
.await?
};
Ok(maybe_voices)
}
struct InternalRawVoiceRecordForList {
token: ZsVoiceToken,
title: String,
ietf_language_tag: String,
ietf_primary_language_subtag: String,
maybe_creator_user_token: Option<String>,
creator_set_visibility: Visibility,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
@@ -1 +1,5 @@
pub mod create_voice;
pub mod delete_voice;
pub mod get_voice;
pub mod update_voice;
pub mod list_voices;
@@ -0,0 +1,74 @@
use anyhow::anyhow;
use sqlx::MySqlPool;
use enums::common::visibility::Visibility;
use enums::by_table::generic_synthetic_ids::id_category::IdCategory;
use errors::AnyhowResult;
use tokens::tokens::zs_voice::ZsVoiceToken;
use tokens::users::user::UserToken;
use crate::queries::generic_synthetic_ids::transactional_increment_generic_synthetic_id::transactional_increment_generic_synthetic_id;
pub struct UpdateVoiceArgs<'a> {
pub voice_token: &'a ZsVoiceToken,
pub voice_title: Option<&'a str>,
pub maybe_creator_user_token: Option<&'a str>,
pub creator_ip_address: &'a str,
pub creator_set_visibility: &'a Visibility,
pub maybe_mod_user_token: Option<&'a str>,
pub ietf_language_tag: Option<&'a str>,
pub ietf_primary_language_subtag: Option<&'a str>,
pub mysql_pool: &'a MySqlPool
}
pub async fn update_voice(args: UpdateVoiceArgs<'_>) -> AnyhowResult<()>{
// TODO: enforce checks for idempotency token
let mut maybe_creator_synthetic_id : Option<u64> = None;
let mut transaction = args.mysql_pool.begin().await?;
if let Some(creator_user_token) = args.maybe_creator_user_token.as_deref() {
let user_token = UserToken::new_from_str(creator_user_token);
let next_zs_voice_synthetic_id = transactional_increment_generic_synthetic_id(
&user_token,
IdCategory::ZsVoice,
&mut transaction
).await?;
maybe_creator_synthetic_id = Some(next_zs_voice_synthetic_id);
}
let query_result = sqlx::query!(
r#"
UPDATE zs_voices
SET
title = ?,
maybe_creator_user_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
maybe_mod_user_token = ?,
maybe_creator_synthetic_id = ?,
ietf_language_tag = ?,
ietf_primary_language_subtag = ?,
version = version + 1
WHERE token = ?
LIMIT 1
"#,
args.voice_title,
args.maybe_creator_user_token,
args.creator_ip_address,
args.creator_set_visibility.to_str(),
args.maybe_mod_user_token,
maybe_creator_synthetic_id,
args.ietf_language_tag,
args.ietf_primary_language_subtag,
args.voice_token.as_str(),
).execute(args.mysql_pool).await;
// TODO(Kasisnu): This should probably rollback
transaction.commit().await?;
match query_result {
Ok(_) => Ok(()),
Err(err) => Err(anyhow!("zs voice update error: {:?}", err)),
}
}
@@ -76,8 +76,6 @@ pub async fn create_dataset_handler(http_request: HttpRequest, request: web::Jso
},
};
let is_mod = user_session.can_ban_users;
let idempotency_token = request.idempotency_token.clone().ok_or(CreateDatasetError::BadInput("no idempotency token provided".to_string()))?;
let title = request.title.clone();
@@ -86,18 +84,11 @@ pub async fn create_dataset_handler(http_request: HttpRequest, request: web::Jso
let creator_set_visibility = request.creator_set_visibility.unwrap_or(Visibility::Public);
let mut maybe_mod_user_token = None;
if is_mod {
maybe_mod_user_token = Some(user_session.user_token.clone());
}
let query_result = create_dataset(CreateDatasetArgs {
dataset_title: &title,
maybe_creator_user_token: Some(user_session.user_token.clone().as_ref()),
creator_ip_address: &creator_ip_address,
creator_set_visibility: &creator_set_visibility,
maybe_mod_user_token: maybe_mod_user_token.as_deref(),
mysql_pool: &server_state.mysql_pool
}).await;
@@ -110,7 +110,6 @@ pub async fn delete_dataset_handler(
}
};
//TODO(kasisnu): Confirm who can delete/undelete and cleanup this logic
let is_creator = dataset.maybe_creator_user_token.as_deref()
.map(|creator_user_token| creator_user_token == &user_session.user_token)
.unwrap_or(false);
@@ -145,12 +144,11 @@ pub async fn delete_dataset_handler(
} else {
match delete_role {
DeleteRole::ErrorDoNotDelete => {
warn!("user is not allowed to undelete inference results: {}", user_session.user_token);
warn!("user is not allowed to undelete voices: {}", user_session.user_token);
return Err(DeleteDatasetError::NotAuthorized);
}
DeleteRole::AsUser => {
// NB: Technically only mods can see their own datasets
//TODO(kasisnu): Is this a case that can apply to datasets?
undelete_dataset_as_user(
&path.dataset_token,
&server_state.mysql_pool
@@ -176,5 +174,4 @@ pub async fn delete_dataset_handler(
Ok(simple_json_success())
}
}
@@ -1,13 +1,175 @@
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, log, warn};
use enums::by_table::audit_logs::audit_log_entity_action::AuditLogEntityAction::Delete;
use log::{info, warn};
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use mysql_queries::queries::voice_designer::voices::delete_voice::{delete_voice_as_mod, delete_voice_as_user, undelete_voice_as_mod, undelete_voice_as_user};
use mysql_queries::queries::voice_designer::voices::get_voice::get_voice_by_token;
use tokens::tokens::zs_voice::ZsVoiceToken;
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
pub async fn delete_voice(voice_token: web::Path<String>) -> HttpResponse {
// Implementation for deleting a voice (soft delete)
HttpResponse::Ok().json(format!("Voice {} deleted (soft delete)", voice_token))
use crate::server_state::ServerState;
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
#[derive(Deserialize)]
pub struct DeleteVoiceRequest {
set_delete: bool,
/// NB: this is only to disambiguate when a user is both a mod and an author.
as_mod: Option<bool>,
}
/// For the URL PathInfo
#[derive(Deserialize)]
pub struct DeleteVoicePathInfo {
voice_token: String,
}
// =============== Error Response ===============
#[derive(Debug, Serialize)]
pub enum DeleteVoiceError {
BadInput(String),
NotFound,
NotAuthorized,
ServerError,
}
impl ResponseError for DeleteVoiceError {
fn status_code(&self) -> StatusCode {
match *self {
DeleteVoiceError::BadInput(_) => StatusCode::BAD_REQUEST,
DeleteVoiceError::NotFound => StatusCode::NOT_FOUND,
DeleteVoiceError::NotAuthorized => StatusCode::UNAUTHORIZED,
DeleteVoiceError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
serialize_as_json_error(self)
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for DeleteVoiceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
// =============== Handler ===============
pub async fn delete_voice_handler(
http_request: HttpRequest,
path: Path<DeleteVoicePathInfo>,
request: web::Json<DeleteVoiceRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, DeleteVoiceError>{
let maybe_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);
DeleteVoiceError::ServerError
})?;
let user_session = match maybe_user_session {
Some(session) => session,
None => {
warn!("not logged in");
return Err(DeleteVoiceError::NotAuthorized);
}
};
let voice_token = path.voice_token.clone();
let is_mod = user_session.can_ban_users;
let voice_lookup_result = get_voice_by_token(
&ZsVoiceToken::new(voice_token.clone()),
is_mod,
&server_state.mysql_pool,
).await;
let voice = match voice_lookup_result {
Ok(Some(voice)) => voice,
Ok(None) => {
warn!("Voice not found: {:?}", voice_token);
return Err(DeleteVoiceError::NotFound);
},
Err(err) => {
warn!("Error looking up voice: {:?}", err);
return Err(DeleteVoiceError::ServerError);
}
};
let is_creator = voice.maybe_creator_user_token.as_deref()
.map(|creator_user_token| creator_user_token == &user_session.user_token)
.unwrap_or(false);
if !is_creator && !is_mod {
warn!("user is not allowed to delete this voice: {}", user_session.user_token);
return Err(DeleteVoiceError::NotAuthorized);
}
let delete_role = delete_role_disambiguation(is_mod, is_creator, request.as_mod);
let query_result = if request.set_delete {
match delete_role {
DeleteRole::ErrorDoNotDelete => {
warn!("user is not allowed to delete voices: {}", user_session.user_token);
return Err(DeleteVoiceError::NotAuthorized);
}
DeleteRole::AsUser => {
delete_voice_as_user(
&path.voice_token,
&server_state.mysql_pool
).await
}
DeleteRole::AsMod => {
delete_voice_as_mod(
&path.voice_token,
&user_session.user_token,
&server_state.mysql_pool
).await
}
}
} else {
match delete_role {
DeleteRole::ErrorDoNotDelete => {
warn!("user is not allowed to undelete voices: {}", user_session.user_token);
return Err(DeleteVoiceError::NotAuthorized);
}
DeleteRole::AsUser => {
undelete_voice_as_user(
&path.voice_token,
&server_state.mysql_pool
).await
}
DeleteRole::AsMod => {
undelete_voice_as_mod(
&path.voice_token,
&user_session.user_token,
&server_state.mysql_pool
).await
}
}
};
match query_result {
Ok(_) => {},
Err(err) => {
warn!("Update voice mod approval status DB error: {:?}", err);
return Err(DeleteVoiceError::ServerError);
}
};
Ok(simple_json_success())
}
@@ -5,9 +5,13 @@ 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::{info, warn};
use mysql_queries::queries::voice_designer::datasets::list_datasets::{ list_datasets_by_user_token, list_datasets_with_connection};
use crate::server_state::ServerState;
#[derive(Serialize, Clone)]
pub struct ZsDatasetRecordForResponse {
@@ -29,6 +33,11 @@ pub struct ListDatasetsByUserSuccessResponse {
pub datasets: Vec<ZsDatasetRecordForResponse>,
}
#[derive(Deserialize)]
pub struct ListDatasetsByUserPathInfo {
user_token: String,
}
#[derive(Debug)]
pub enum ListDatasetsByUserError {
NotAuthorized,
@@ -50,16 +59,73 @@ impl ResponseError for ListDatasetsByUserError {
}
}
pub async fn list_datasets_by_user_handler(user_token: web::Path<String>) -> HttpResponse {
pub async fn list_datasets_by_user_handler(
http_request: HttpRequest,
path: Path<ListDatasetsByUserPathInfo>,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, ListDatasetsByUserError> {
//TOOD(kasisnu):
// [ ] confirm if we need a feature flag to disable this endpoint
// [ ] check if this needs to be cached in Redis/anywhere - what's the diff between ephemeral/durable
// [ ] double check if the fields in the struct are everything needed for the FE
// [ ] Mocks have a few datapoints I think I didn't track anywhere - stars/likes/plays/favorites
let maybe_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);
ListDatasetsByUserError::ServerError
})?;
let user_session = match maybe_user_session {
Some(session) => session,
None => {
warn!("not logged in");
return Err(ListDatasetsByUserError::NotAuthorized);
}
};
let user_token = path.user_token.clone();
let creator_user_token = user_session.user_token.clone();
let is_mod = user_session.can_ban_users;
// Implementation for listing datasets for a user
HttpResponse::Ok().json(format!("List of datasets for user {}", user_token))
let query_results = list_datasets_by_user_token(
&server_state.mysql_pool,
&user_token,
is_mod,
creator_user_token == user_token,
).await.map_err(|e| {
warn!("Error querying for datasets: {:?}", e);
ListDatasetsByUserError::ServerError
});
let datasets = match query_results {
Ok(datasets) => datasets,
Err(e) => {
warn!("Error querying for datasets: {:?}", e);
return Err(ListDatasetsByUserError::ServerError);
}
};
let datasets = datasets.into_iter().map(|dataset| {
ZsDatasetRecordForResponse {
dataset_token: dataset.dataset_token,
title: dataset.title,
creator_set_visibility: dataset.creator_set_visibility.to_string() ,
ietf_language_tag: dataset.ietf_language_tag,
ietf_primary_language_subtag: dataset.ietf_primary_language_subtag,
maybe_creator_user_token: dataset.maybe_creator_user_token,
created_at: dataset.created_at,
updated_at: dataset.updated_at,
}
}).collect();
let response = ListDatasetsByUserSuccessResponse {
success: true,
datasets,
};
let body = serde_json::to_string(&response)
.map_err(|e| ListDatasetsByUserError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
@@ -1,14 +1,134 @@
use std::fmt;
use std::fmt::{Formatter};
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::{info, warn};
use mysql_queries::queries::voice_designer::voices::list_voices::list_voices_by_user_token;
use crate::server_state::ServerState;
#[derive(Serialize, Clone)]
pub struct ZsVoiceRecordForResponse {
voice_token: String,
title: String,
creator_set_visibility: String,
ietf_language_tag: String,
ietf_primary_language_subtag: String,
maybe_creator_user_token: Option<String>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
#[derive(Serialize)]
pub struct ListVoicesByUserSuccessResponse {
pub success: bool,
pub voices: Vec<ZsVoiceRecordForResponse>,
}
#[derive(Deserialize)]
pub struct ListVoicesByUserPathInfo {
user_token: String,
}
#[derive(Debug)]
pub enum ListVoicesByUserError {
NotAuthorized,
ServerError,
}
impl fmt::Display for ListVoicesByUserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
impl ResponseError for ListVoicesByUserError {
fn status_code(&self) -> StatusCode {
match *self {
ListVoicesByUserError::NotAuthorized => StatusCode::UNAUTHORIZED,
ListVoicesByUserError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
pub async fn list_voices_by_user(
http_request: HttpRequest,
path: Path<ListVoicesByUserPathInfo>,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, ListVoicesByUserError> {
let maybe_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);
ListVoicesByUserError::ServerError
})?;
let user_session = match maybe_user_session {
Some(session) => session,
None => {
warn!("not logged in");
return Err(ListVoicesByUserError::NotAuthorized);
}
};
let user_token = path.user_token.clone();
let creator_user_token = user_session.user_token.clone();
let is_mod = user_session.can_ban_users;
let query_results = list_voices_by_user_token(
&server_state.mysql_pool,
&user_token,
is_mod,
creator_user_token == user_token,
).await.map_err(|e| {
warn!("Error querying for voices: {:?}", e);
ListVoicesByUserError::ServerError
});
let voices = match query_results {
Ok(voices) => voices,
Err(e) => {
warn!("Error querying for voices: {:?}", e);
return Err(ListVoicesByUserError::ServerError);
}
};
let voices = voices.into_iter().map(|voice| {
ZsVoiceRecordForResponse {
voice_token: voice.voice_token,
title: voice.title,
creator_set_visibility: voice.creator_set_visibility.to_string() ,
ietf_language_tag: voice.ietf_language_tag,
ietf_primary_language_subtag: voice.ietf_primary_language_subtag,
maybe_creator_user_token: voice.maybe_creator_user_token,
created_at: voice.created_at,
updated_at: voice.updated_at,
}
}).collect();
let response = ListVoicesByUserSuccessResponse {
success: true,
voices,
};
let body = serde_json::to_string(&response)
.map_err(|e| ListVoicesByUserError::ServerError)?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
pub async fn list_voices_by_user(user_token: web::Path<String>) -> HttpResponse {
// Implementation for listing voices for a user
HttpResponse::Ok().json(format!("List of voices for user {}", user_token))
}
@@ -1,13 +1,202 @@
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, log, warn};
use enums::common::visibility::Visibility;
use log::{info, warn};
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::response_success_helpers::simple_json_success;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use mysql_queries::queries::voice_designer::voices::create_voice::{create_voice, CreateVoiceArgs};
use mysql_queries::queries::voice_designer::voices::get_voice::get_voice_by_token;
use mysql_queries::queries::voice_designer::voices::update_voice::{update_voice, UpdateVoiceArgs};
use tokens::tokens::zs_voice::ZsVoiceToken;
use crate::server_state::ServerState;
pub async fn update_voice(voice_token: web::Path<String>) -> HttpResponse {
// Implementation for updating a voice
HttpResponse::Ok().json(format!("Voice {} updated successfully", voice_token))
#[derive(Deserialize)]
pub struct UpdateVoiceRequest {
// ========== Author + Moderator options ==========
pub title: Option<String>,
pub creator_set_visibility: Option<String>,
// NB: We calculate 'ietf_primary_language_subtag' from this value.
pub ietf_language_tag: Option<String>,
}
#[derive(Serialize)]
pub struct UpdateVoiceResponse {
pub success: bool,
}
/// For the URL PathInfo
#[derive(Deserialize)]
pub struct UpdateVoicePathInfo {
voice_token: String,
}
// =============== Error Response ===============
#[derive(Debug, Serialize)]
pub enum UpdateVoiceError {
BadInput(String),
NotFound,
NotAuthorized,
ServerError,
}
impl ResponseError for UpdateVoiceError {
fn status_code(&self) -> StatusCode {
match *self {
UpdateVoiceError::BadInput(_) => StatusCode::BAD_REQUEST,
UpdateVoiceError::NotFound => StatusCode::NOT_FOUND,
UpdateVoiceError::NotAuthorized => StatusCode::UNAUTHORIZED,
UpdateVoiceError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
serialize_as_json_error(self)
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for UpdateVoiceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
// =============== Handler ===============
pub async fn update_voice_handler(
http_request: HttpRequest,
path: Path<UpdateVoicePathInfo>,
request: web::Json<UpdateVoiceRequest>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, UpdateVoiceError>
{
let maybe_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);
UpdateVoiceError::ServerError
})?;
let user_session = match maybe_user_session {
Some(session) => session,
None => {
warn!("not logged in");
return Err(UpdateVoiceError::NotAuthorized);
}
};
let voice_token = path.voice_token.clone();
let is_mod = user_session.can_ban_users;
let voice_lookup_result = get_voice_by_token(
&ZsVoiceToken::new(voice_token.clone()),
is_mod,
&server_state.mysql_pool,
).await;
let voice = match voice_lookup_result {
Ok(Some(voice)) => voice,
Ok(None) => {
warn!("Voice not found: {:?}", voice_token);
return Err(UpdateVoiceError::NotFound);
},
Err(err) => {
warn!("Error looking up voice: {:?}", err);
return Err(UpdateVoiceError::ServerError);
}
};
// let is_creator = voice.maybe_creator_user_token == Some(user_session.user_token);
let is_creator = voice.maybe_creator_user_token.as_deref()
.map(|creator_user_token| creator_user_token == &user_session.user_token)
.unwrap_or(false);
if !is_creator && !is_mod {
warn!("user is not allowed to edit this voice: {}", user_session.user_token);
return Err(UpdateVoiceError::NotAuthorized);
}
let mut title = None;
let mut ietf_language_tag = None;
let mut ietf_primary_language_subtag = None;
let mut creator_set_visibility = Visibility::Public;
if let Some(payload) = request.title.as_deref() {
if user_input_common::check_for_slurs::contains_slurs(payload) {
return Err(UpdateVoiceError::BadInput("title contains slurs".to_string()));
}
title = Some(payload.trim().to_string());
}
if let Some(tag) = request.ietf_language_tag.as_deref() {
// eg. en, en-US, es-419, ja-JP, etc.
let maybe_full_canonical_tag = container_common::i18n::supported_languages_for_models::get_canonicalized_language_tag_for_model(tag);
// eg. en, es, ja, etc.
let maybe_primary_language_subtag = maybe_full_canonical_tag
.map(|t| language_tags::LanguageTag::parse(t)
.map(|language_tag| language_tag.primary_language().to_string())
)
.transpose()
.map_err(|e| {
error!("Error parsing language tag '{}': {:?}", tag, e);
UpdateVoiceError::BadInput("bad locale string".to_string())
})?;
if let Some(full_tag) = maybe_full_canonical_tag {
if let Some(primary_subtag) = maybe_primary_language_subtag.as_deref() {
ietf_language_tag = Some(full_tag.to_string());
ietf_primary_language_subtag = Some(primary_subtag.to_string());
}
}
}
if let Some(visibility) = request.creator_set_visibility.as_deref() {
creator_set_visibility = Visibility::from_str(visibility)
.map_err(|_| UpdateVoiceError::BadInput("bad record visibility".to_string()))?;
}
let ip_address = get_request_ip(&http_request);
let mut maybe_mod_user_token = None;
if is_mod {
maybe_mod_user_token = Some(user_session.user_token.clone());
}
let query_result = update_voice(
UpdateVoiceArgs {
voice_token: &ZsVoiceToken::new(voice_token.clone()),
voice_title: title.as_deref(),
maybe_creator_user_token: Some(user_session.user_token.clone().as_ref()),
creator_ip_address: ip_address.as_ref(),
creator_set_visibility: &creator_set_visibility,
maybe_mod_user_token: maybe_mod_user_token.as_deref(),
ietf_language_tag: ietf_language_tag.as_deref(),
ietf_primary_language_subtag: ietf_primary_language_subtag.as_deref(),
mysql_pool: &server_state.mysql_pool
}
).await;
match query_result {
Ok(_) => {},
Err(err) => {
warn!("Update Voice DB error: {:?}", err);
return Err(UpdateVoiceError::ServerError);
}
};
Ok(simple_json_success())
}
@@ -126,8 +126,8 @@ use crate::http_server::endpoints::voice_designer::delete_dataset::delete_datase
use crate::http_server::endpoints::voice_designer::list_datasets_by_user::list_datasets_by_user_handler;
use crate::http_server::endpoints::voice_designer::create_voice::create_voice_handler;
use crate::http_server::endpoints::voice_designer::update_voice::update_voice;
use crate::http_server::endpoints::voice_designer::delete_voice::delete_voice;
use crate::http_server::endpoints::voice_designer::update_voice::update_voice_handler;
use crate::http_server::endpoints::voice_designer::delete_voice::delete_voice_handler;
use crate::http_server::endpoints::voice_designer::list_available_voices::list_available_voices;
use crate::http_server::endpoints::voice_designer::upload_sample::upload_sample;
@@ -1151,8 +1151,8 @@ fn add_voice_designer_routes<T,B> (app:App<T>)-> App<T>
.service(
web::scope("/voice")
.route("/create", web::post().to(create_voice_handler))
.route("/{voice_token}/update", web::post().to(update_voice))
.route("/{voice_token}/delete", web::delete().to(delete_voice))
.route("/{voice_token}/update", web::post().to(update_voice_handler))
.route("/{voice_token}/delete", web::delete().to(delete_voice_handler))
.route("/user/{user_token}/list", web::get().to(list_voices_by_user))
)
.service(