diff --git a/src/common_queries/mod.rs b/src/common_queries/mod.rs index 8feffdc37f..1caba8130d 100644 --- a/src/common_queries/mod.rs +++ b/src/common_queries/mod.rs @@ -4,6 +4,7 @@ pub mod list_tts_inference_results; pub mod list_tts_models; pub mod list_w2l_inference_results; pub mod list_w2l_templates; +pub mod query_tts_model; pub mod query_user_profile; pub mod query_w2l_template; pub mod sessions; \ No newline at end of file diff --git a/src/common_queries/query_tts_model.rs b/src/common_queries/query_tts_model.rs new file mode 100644 index 0000000000..8184d4be54 --- /dev/null +++ b/src/common_queries/query_tts_model.rs @@ -0,0 +1,171 @@ +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use crate::AnyhowResult; +use crate::database_helpers::boolean_converters::i8_to_bool; +use crate::database_helpers::boolean_converters::nullable_i8_to_optional_bool; +use derive_more::{Display, Error}; +use log::{info, warn, log}; +use regex::Regex; +use sqlx::MySqlPool; +use sqlx::error::DatabaseError; +use sqlx::error::Error::Database; +use sqlx::mysql::MySqlDatabaseError; +use std::sync::Arc; + +#[derive(Serialize)] +pub struct TtsModelRecordForResponse { + pub model_token: String, + pub tts_model_type: String, + pub creator_user_token: String, + pub creator_username: String, + pub creator_display_name: String, + pub updatable_slug: String, + pub title: String, + pub description_markdown: String, + pub description_rendered_html: String, + pub is_mod_disabled: bool, + pub is_locked_from_user_modification: bool, + pub created_at: DateTime, + pub updated_at: DateTime, + pub user_deleted_at: Option>, + pub mod_deleted_at: Option>, +} + +#[derive(Serialize)] +pub struct TtsModelRecordRaw { + pub model_token: String, + pub tts_model_type: String, + pub creator_user_token: String, + pub creator_username: String, + pub creator_display_name: String, + pub updatable_slug: String, + pub title: String, + pub description_markdown: String, + pub description_rendered_html: String, + pub is_mod_disabled: i8, + pub is_locked_from_user_modification: i8, + pub created_at: DateTime, + pub updated_at: DateTime, + pub user_deleted_at: Option>, + pub mod_deleted_at: Option>, +} + +pub async fn select_tts_model_by_token( + tts_model_token: &str, + can_see_deleted: bool, + mysql_pool: &MySqlPool +) -> AnyhowResult> { + + let maybe_record = if can_see_deleted { + select_including_deleted(tts_model_token, mysql_pool).await + } else { + select_without_deleted(tts_model_token, mysql_pool).await + }; + + let model : TtsModelRecordRaw = match maybe_record { + Ok(model) => model, + Err(ref err) => { + match err { + RowNotFound => { + warn!("tts model not found: {:?}", &err); + return Ok(None); + }, + _ => { + warn!("tts model query error: {:?}", &err); + return Err(anyhow!("database error")); + } + } + } + }; + + let model_for_response = TtsModelRecordForResponse { + model_token: model.model_token.clone(), + tts_model_type: model.tts_model_type.clone(), + creator_user_token: model.creator_user_token.clone(), + creator_username: model.creator_username.clone(), + creator_display_name: model.creator_display_name.clone(), + updatable_slug: model.updatable_slug.clone(), + title: model.title.clone(), + description_markdown: model.description_markdown.clone(), + description_rendered_html: model.description_rendered_html.clone(), + is_mod_disabled: i8_to_bool(model.is_mod_disabled), + is_locked_from_user_modification: i8_to_bool(model.is_locked_from_user_modification), + created_at: model.created_at.clone(), + updated_at: model.updated_at.clone(), + user_deleted_at: model.user_deleted_at.clone(), + mod_deleted_at: model.mod_deleted_at.clone(), + }; + + Ok(Some(model_for_response)) +} + +async fn select_including_deleted( + tts_model_token: &str, + mysql_pool: &MySqlPool +) -> Result { + sqlx::query_as!( + TtsModelRecordRaw, + r#" +SELECT + tts.token as model_token, + tts.tts_model_type, + tts.creator_user_token, + users.username as creator_username, + users.display_name as creator_display_name, + tts.updatable_slug, + tts.title, + tts.description_markdown, + tts.description_rendered_html, + tts.is_mod_disabled, + tts.is_locked_from_user_modification, + tts.created_at, + tts.updated_at, + tts.user_deleted_at, + tts.mod_deleted_at +FROM tts_models as tts +JOIN users + ON users.token = tts.creator_user_token +WHERE tts.token = ? + "#, + tts_model_token + ) + .fetch_one(mysql_pool) + .await // TODO: This will return error if it doesn't exist +} + +async fn select_without_deleted( + tts_model_token: &str, + mysql_pool: &MySqlPool +) -> Result { + sqlx::query_as!( + TtsModelRecordRaw, + r#" +SELECT + tts.token as model_token, + tts.tts_model_type, + tts.creator_user_token, + users.username as creator_username, + users.display_name as creator_display_name, + tts.updatable_slug, + tts.title, + tts.description_markdown, + tts.description_rendered_html, + tts.is_mod_disabled, + tts.is_locked_from_user_modification, + tts.created_at, + tts.updated_at, + tts.user_deleted_at, + tts.mod_deleted_at +FROM tts_models as tts +JOIN users + ON users.token = tts.creator_user_token +WHERE + tts.token = ? + AND tts.user_deleted_at IS NULL + AND tts.mod_deleted_at IS NULL + "#, + tts_model_token + ) + .fetch_one(mysql_pool) + .await // TODO: This will return error if it doesn't exist +} diff --git a/src/http_server/endpoints/tts/edit_tts_model.rs b/src/http_server/endpoints/tts/edit_tts_model.rs new file mode 100644 index 0000000000..2b4a4d2be8 --- /dev/null +++ b/src/http_server/endpoints/tts/edit_tts_model.rs @@ -0,0 +1,234 @@ +use actix_http::Error; +use actix_http::http::header; +use actix_web::cookie::Cookie; +use actix_web::dev::HttpResponseBuilder; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Path; +use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use crate::common_queries::query_tts_model::select_tts_model_by_token; +use crate::common_queries::sessions::create_session_for_user; +use crate::http_server::web_utils::ip_address::get_request_ip; +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; +use crate::util::email_to_gravatar::email_to_gravatar; +use crate::util::markdown_to_html::markdown_to_html; +use crate::util::random_crockford_token::random_crockford_token; +use crate::util::random_prefix_crockford_token::random_prefix_crockford_token; +use crate::validations::check_for_slurs::contains_slurs; +use derive_more::{Display, Error}; +use log::{info, warn, log}; +use regex::Regex; +use sqlx::error::DatabaseError; +use sqlx::error::Error::Database; +use sqlx::mysql::MySqlDatabaseError; +use std::sync::Arc; + +/// For the URL PathInfo +#[derive(Deserialize)] +pub struct EditTtsModelPathInfo { + model_token: String, +} + +#[derive(Deserialize)] +pub struct EditTtsModelRequest { + pub title: Option, + pub description_markdown: Option, + + //pub updatable_slug: Option, + //pub creator_set_visibility: Option, + + //pub tts_model_type: Option, + //pub text_preprocessing_algorithm: Option, + //pub vocoder_token: Option, + + //pub is_mod_disabled: Option, + //pub is_locked_from_user_modification: Option, + //pub maybe_mod_comments: Option, +} + +#[derive(Debug, Display)] +pub enum EditTtsModelError { + BadInput(String), + NotAuthorized, + TemplateNotFound, + ServerError, +} + +impl ResponseError for EditTtsModelError { + fn status_code(&self) -> StatusCode { + match *self { + EditTtsModelError::BadInput(_) => StatusCode::BAD_REQUEST, + EditTtsModelError::NotAuthorized => StatusCode::UNAUTHORIZED, + EditTtsModelError::TemplateNotFound => StatusCode::NOT_FOUND, + EditTtsModelError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + EditTtsModelError::BadInput(reason) => reason.to_string(), + EditTtsModelError::NotAuthorized=> "unauthorized".to_string(), + EditTtsModelError::TemplateNotFound => "not found".to_string(), + EditTtsModelError::ServerError => "server error".to_string(), + }; + + to_simple_json_error(&error_reason, self.status_code()) + } +} + +pub async fn edit_tts_model_handler( + http_request: HttpRequest, + path: Path, + request: web::Json, + server_state: web::Data>) -> Result +{ + 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); + EditTtsModelError::ServerError + })?; + + let user_session = match maybe_user_session { + Some(session) => session, + None => { + warn!("not logged in"); + return Err(EditTtsModelError::NotAuthorized); + } + }; + + // NB: First permission check. + // Only mods should see deleted models (both user_* and mod_* deleted). + let is_mod_that_can_see_deleted = user_session.can_delete_other_users_tts_models; + + let model_lookup_result = select_tts_model_by_token( + &path.model_token, + is_mod_that_can_see_deleted, + &server_state.mysql_pool).await; + + let model_record = match model_lookup_result { + Ok(Some(result)) => { + info!("Found model: {}", result.model_token); + result + }, + Ok(None) => { + warn!("could not find model"); + return Err(EditTtsModelError::TemplateNotFound); + }, + Err(err) => { + warn!("error looking up model: {:?}", err); + return Err(EditTtsModelError::TemplateNotFound); + }, + }; + + // NB: Second set of permission checks + let mut editor_is_original_user = false; + let mut editor_is_moderator = false; + + if model_record.creator_user_token == user_session.user_token { + editor_is_original_user = true; + } + + if user_session.can_edit_other_users_tts_models { + editor_is_moderator = true; + } + + if !editor_is_original_user && !editor_is_moderator { + return Err(EditTtsModelError::NotAuthorized); + } + + if !editor_is_moderator { + if model_record.is_locked_from_user_modification || model_record.is_mod_disabled { + return Err(EditTtsModelError::NotAuthorized); + } + } + + // Fields to set + let mut title = None; + let mut description_markdown = None; + let mut description_html = None; + + if let Some(payload) = request.title.as_deref() { + if contains_slurs(payload) { + return Err(EditTtsModelError::BadInput("title contains slurs".to_string())); + } + + title = Some(payload.to_string()); + } + + if let Some(markdown) = request.description_markdown.as_deref() { + if contains_slurs(markdown) { + return Err(EditTtsModelError::BadInput("description contains slurs".to_string())); + } + + let markdown = markdown.trim().to_string(); + let html = markdown_to_html(&markdown); + + description_markdown = Some(markdown); + description_html = Some(html); + } + + let ip_address = get_request_ip(&http_request); + + let query_result = if editor_is_original_user { + // We need to store the IP address details. + sqlx::query!( + r#" +UPDATE tts_models +SET + title = ?, + description_markdown = ?, + description_rendered_html = ?, + creator_ip_address_last_update = ?, + version = version + 1 + +WHERE tts_models.token = ? +LIMIT 1 + "#, + &title, + &description_markdown, + &description_html, + &ip_address, + &model_record.model_token, + ) + .execute(&server_state.mysql_pool) + .await + } else { + // We need to store the moderator details. + sqlx::query!( + r#" +UPDATE tts_models +SET + title = ?, + description_markdown = ?, + description_rendered_html = ?, + maybe_mod_user_token = ?, + version = version + 1 + +WHERE tts_models.token = ? +LIMIT 1 + "#, + &title, + &description_markdown, + &description_html, + &user_session.user_token, + &model_record.model_token, + ) + .execute(&server_state.mysql_pool) + .await + }; + + match query_result { + Ok(_) => {}, + Err(err) => { + warn!("Update W2L model edit DB error: {:?}", err); + return Err(EditTtsModelError::ServerError); + } + }; + + Ok(simple_json_success()) +} diff --git a/src/http_server/endpoints/tts/mod.rs b/src/http_server/endpoints/tts/mod.rs index bdff9c584d..20d7957abf 100644 --- a/src/http_server/endpoints/tts/mod.rs +++ b/src/http_server/endpoints/tts/mod.rs @@ -1,3 +1,4 @@ +pub mod edit_tts_model; pub mod enqueue_infer_tts; pub mod enqueue_upload_tts_model; pub mod get_tts_inference_job_status; diff --git a/src/main.rs b/src/main.rs index 9377b4dca1..c2e467aafe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,6 +71,7 @@ use crate::http_server::endpoints::users::edit_profile::edit_profile_handler; use crate::http_server::endpoints::w2l::set_w2l_template_mod_approval::set_w2l_template_mod_approval_handler; use crate::http_server::endpoints::w2l::delete_w2l_template::delete_w2l_template_handler; use crate::http_server::endpoints::w2l::edit_w2l_template::edit_w2l_template_handler; +use crate::http_server::endpoints::tts::edit_tts_model::edit_tts_model_handler; // TODO TODO TODO TODO // TODO TODO TODO TODO @@ -293,6 +294,11 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()> .route(web::get().to(get_tts_model_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service( + web::resource("/model/{model_token}/edit") + .route(web::post().to(edit_tts_model_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) .service( web::resource("/result/{token}") .route(web::get().to(get_tts_inference_result_handler)) @@ -333,7 +339,7 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()> .route(web::head().to(|| HttpResponse::Ok())) ) .service( - web::resource("/template/{template_token}/edit") + web::resource("/template/{template_token}/edit") .route(web::post().to(edit_w2l_template_handler)) .route(web::head().to(|| HttpResponse::Ok())) )