From e6f0908a3db3bbac0e32397e36487d90cca98ddd Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 19 Feb 2022 13:48:36 -0500 Subject: [PATCH] voice clone requests endpoints --- .../up.sql | 11 +- .../get_voice_clone_request.rs | 41 +++-- .../insert_voice_clone_requests_builder.rs | 25 +-- .../src/http_server/endpoints/mod.rs | 1 + .../check_if_voice_clone_request_submitted.rs | 130 ++++++++++++++ .../create_voice_clone_request.rs | 158 ++++++++++++++++++ .../endpoints/voice_clone_requests/mod.rs | 2 + crates/service/storyteller_web/src/routes.rs | 29 ++++ 8 files changed, 363 insertions(+), 34 deletions(-) create mode 100644 crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs create mode 100644 crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs create mode 100644 crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs diff --git a/_migrations/2022-02-19-004032_voice_clone_requests/up.sql b/_migrations/2022-02-19-004032_voice_clone_requests/up.sql index a1435d7d44..e59d133c09 100644 --- a/_migrations/2022-02-19-004032_voice_clone_requests/up.sql +++ b/_migrations/2022-02-19-004032_voice_clone_requests/up.sql @@ -25,6 +25,10 @@ CREATE TABLE voice_clone_requests( -- ========== REASON ========== + -- Visibility + is_for_private_use BOOLEAN NOT NULL DEFAULT FALSE, + is_for_public_use BOOLEAN NOT NULL DEFAULT FALSE, + -- How they'll use is_for_studio BOOLEAN NOT NULL DEFAULT FALSE, is_for_twitch_tts BOOLEAN NOT NULL DEFAULT FALSE, @@ -34,13 +38,10 @@ CREATE TABLE voice_clone_requests( is_for_other BOOLEAN NOT NULL DEFAULT FALSE, optional_notes_on_use MEDIUMTEXT DEFAULT NULL, - -- Visibility - is_for_private_use BOOLEAN NOT NULL DEFAULT FALSE, - is_for_public_use BOOLEAN NOT NULL DEFAULT FALSE, - - -- Ownership + -- Subject/Ownership is_own_voice BOOLEAN NOT NULL DEFAULT FALSE, is_third_party_voice BOOLEAN NOT NULL DEFAULT FALSE, + optional_notes_on_subject VARCHAR(255) DEFAULT NULL, -- ========== RECORDING QUALITY / EQUIPMENT ========== diff --git a/crates/lib/database_queries/src/queries/voice_clone_requests/get_voice_clone_request.rs b/crates/lib/database_queries/src/queries/voice_clone_requests/get_voice_clone_request.rs index 20a3fee668..8da3ac7f51 100644 --- a/crates/lib/database_queries/src/queries/voice_clone_requests/get_voice_clone_request.rs +++ b/crates/lib/database_queries/src/queries/voice_clone_requests/get_voice_clone_request.rs @@ -33,6 +33,10 @@ pub struct VoiceCloneRequest { pub email_address: String, pub discord_username: String, + // Visibility + pub is_for_private_use: bool, + pub is_for_public_use: bool, + // Use pub is_for_studio: bool, pub is_for_twitch_tts: bool, @@ -42,13 +46,10 @@ pub struct VoiceCloneRequest { pub is_for_other: bool, pub optional_notes_on_use: Option, - // Visibility - pub is_for_private_use: bool, - pub is_for_public_use: bool, - - // Ownership + // Subject/Ownership pub is_own_voice: bool, pub is_third_party_voice: bool, + pub optional_notes_on_subject: Option, // Equipment pub has_clean_audio_recordings: bool, @@ -76,6 +77,9 @@ SELECT email_address, discord_username, + is_for_private_use, + is_for_public_use, + is_for_studio, is_for_twitch_tts, is_for_api_use, @@ -84,11 +88,9 @@ SELECT is_for_other, optional_notes_on_use, - is_for_private_use, - is_for_public_use, - is_own_voice, is_third_party_voice, + optional_notes_on_subject, has_clean_audio_recordings, has_good_microphone, @@ -124,6 +126,9 @@ SELECT email_address, discord_username, + is_for_private_use, + is_for_public_use, + is_for_studio, is_for_twitch_tts, is_for_api_use, @@ -132,11 +137,9 @@ SELECT is_for_other, optional_notes_on_use, - is_for_private_use, - is_for_public_use, - is_own_voice, is_third_party_voice, + optional_notes_on_subject, has_clean_audio_recordings, has_good_microphone, @@ -172,6 +175,8 @@ fn handle_result( maybe_user_token: record.maybe_user_token, email_address: record.email_address, discord_username: record.discord_username, + is_for_private_use: i8_to_bool(record.is_for_private_use), + is_for_public_use: i8_to_bool(record.is_for_public_use), is_for_studio: i8_to_bool(record.is_for_studio), is_for_twitch_tts: i8_to_bool(record.is_for_twitch_tts), is_for_api_use: i8_to_bool(record.is_for_api_use), @@ -179,10 +184,9 @@ fn handle_result( is_for_games: i8_to_bool(record.is_for_games), is_for_other: i8_to_bool(record.is_for_other), optional_notes_on_use: record.optional_notes_on_use, - is_for_private_use: i8_to_bool(record.is_for_private_use), - is_for_public_use: i8_to_bool(record.is_for_public_use), is_own_voice: i8_to_bool(record.is_own_voice), is_third_party_voice: i8_to_bool(record.is_third_party_voice), + optional_notes_on_subject: record.optional_notes_on_subject, has_clean_audio_recordings: i8_to_bool(record.has_clean_audio_recordings), has_good_microphone: i8_to_bool(record.has_good_microphone), optional_questions: record.optional_questions, @@ -201,6 +205,10 @@ struct VoiceCloneRequestInternal { pub email_address: String, pub discord_username: String, + // Visibility + pub is_for_private_use: i8, + pub is_for_public_use: i8, + // Use pub is_for_studio: i8, pub is_for_twitch_tts: i8, @@ -210,13 +218,10 @@ struct VoiceCloneRequestInternal { pub is_for_other: i8, pub optional_notes_on_use: Option, - // Visibility - pub is_for_private_use: i8, - pub is_for_public_use: i8, - - // Ownership + // Subject/Ownership pub is_own_voice: i8, pub is_third_party_voice: i8, + pub optional_notes_on_subject: Option, // Equipment pub has_clean_audio_recordings: i8, diff --git a/crates/lib/database_queries/src/queries/voice_clone_requests/insert_voice_clone_requests_builder.rs b/crates/lib/database_queries/src/queries/voice_clone_requests/insert_voice_clone_requests_builder.rs index 59c2052369..009d87c05a 100644 --- a/crates/lib/database_queries/src/queries/voice_clone_requests/insert_voice_clone_requests_builder.rs +++ b/crates/lib/database_queries/src/queries/voice_clone_requests/insert_voice_clone_requests_builder.rs @@ -12,6 +12,10 @@ pub struct InsertVoiceCloneRequestBuilder { pub email_address: String, pub discord_username: String, + // Visibility + pub is_for_private_use: bool, + pub is_for_public_use: bool, + // Use pub is_for_studio: bool, pub is_for_twitch_tts: bool, @@ -21,13 +25,10 @@ pub struct InsertVoiceCloneRequestBuilder { pub is_for_other: bool, pub optional_notes_on_use: Option, - // Visibility - pub is_for_private_use: bool, - pub is_for_public_use: bool, - - // Ownership + // Subject/Ownership pub is_own_voice: bool, pub is_third_party_voice: bool, + pub optional_notes_on_subject: Option, // Equipment pub has_clean_audio_recordings: bool, @@ -58,6 +59,9 @@ SET email_address = ?, discord_username = ?, + is_for_private_use = ?, + is_for_public_use = ?, + is_for_studio = ?, is_for_twitch_tts = ?, is_for_api_use = ?, @@ -66,11 +70,9 @@ SET is_for_other = ?, optional_notes_on_use = ?, - is_for_private_use = ?, - is_for_public_use = ?, - is_own_voice = ?, is_third_party_voice = ?, + optional_notes_on_subject = ?, has_clean_audio_recordings = ?, has_good_microphone = ?, @@ -87,6 +89,9 @@ SET &self.email_address, &self.discord_username, + &self.is_for_private_use, + &self.is_for_public_use, + &self.is_for_studio, &self.is_for_twitch_tts, &self.is_for_api_use, @@ -95,11 +100,9 @@ SET &self.is_for_other, &self.optional_notes_on_use, - &self.is_for_private_use, - &self.is_for_public_use, - &self.is_own_voice, &self.is_third_party_voice, + &self.optional_notes_on_subject, &self.has_clean_audio_recordings, &self.has_good_microphone, diff --git a/crates/service/storyteller_web/src/http_server/endpoints/mod.rs b/crates/service/storyteller_web/src/http_server/endpoints/mod.rs index e779339a76..0c8b179f69 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/mod.rs +++ b/crates/service/storyteller_web/src/http_server/endpoints/mod.rs @@ -7,4 +7,5 @@ pub mod moderation; pub mod tts; pub mod twitch; pub mod users; +pub mod voice_clone_requests; pub mod w2l; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs b/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs new file mode 100644 index 0000000000..418cb41748 --- /dev/null +++ b/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs @@ -0,0 +1,130 @@ +use actix_http::Error; +use actix_http::http::header; +use actix_web::HttpResponseBuilder; +use actix_web::cookie::Cookie; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::{Path, Json}; +use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +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 database_queries::queries::voice_clone_requests::get_voice_clone_request::{get_voice_clone_request_by_user_token, get_voice_clone_request_by_token}; +use database_queries::queries::voice_clone_requests::insert_voice_clone_requests_builder::InsertVoiceCloneRequestBuilder; +use http_server_common::response::serialize_as_json_error::serialize_as_json_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::fmt; +use std::sync::Arc; + +// =============== Request =============== + +#[derive(Deserialize)] +pub struct CheckIfVoiceRequestSubmittedRequest { + /// Though we look up logged in users by user token, we can also look up non-logged-in users + /// with a token lookup. We can store this in frontend state or a cookie. + pub maybe_request_token: Option, +} + +// =============== Success Response =============== + +#[derive(Serialize)] +pub struct CheckIfVoiceRequestSubmittedResponse { + pub success: bool, + pub has_submitted: bool, +} + +// =============== Error Response =============== + +#[derive(Debug, Serialize)] +pub enum CheckIfVoiceRequestSubmittedError { + BadInput(String), + NotAuthorized, + ServerError, +} + +impl ResponseError for CheckIfVoiceRequestSubmittedError { + fn status_code(&self) -> StatusCode { + match *self { + CheckIfVoiceRequestSubmittedError::BadInput(_) => StatusCode::BAD_REQUEST, + CheckIfVoiceRequestSubmittedError::NotAuthorized => StatusCode::UNAUTHORIZED, + CheckIfVoiceRequestSubmittedError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } +} + +// NB: Not using DeriveMore since Clion doesn't understand it. +impl fmt::Display for CheckIfVoiceRequestSubmittedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +// =============== Handler =============== + +pub async fn check_if_voice_clone_request_submitted_handler( + http_request: HttpRequest, + 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); + CheckIfVoiceRequestSubmittedError::ServerError + })?; + + let mut submitted_by_token = false; + let mut submitted_by_user = false; + + // Non-logged in accounts (via cookie or frontend mechanism) + if let Some(token) = request.maybe_request_token.as_deref() { + let request = get_voice_clone_request_by_token( + token, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Database error: {:?}", e); + CheckIfVoiceRequestSubmittedError::ServerError + })?; + + submitted_by_token = request.is_some(); + } + + // Logged in users + if let Some(user) = maybe_user_session.as_ref() { + let request = get_voice_clone_request_by_user_token( + &user.user_token, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Database error: {:?}", e); + CheckIfVoiceRequestSubmittedError::ServerError + })?; + + submitted_by_user = request.is_some(); + } + + let has_submitted = submitted_by_token || submitted_by_user; + + let response = CheckIfVoiceRequestSubmittedResponse { + success: true, + has_submitted, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| CheckIfVoiceRequestSubmittedError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs b/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs new file mode 100644 index 0000000000..024b3e1fc0 --- /dev/null +++ b/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs @@ -0,0 +1,158 @@ +use actix_http::Error; +use actix_http::http::header; +use actix_web::HttpResponseBuilder; +use actix_web::cookie::Cookie; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::{Path, Json}; +use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +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 database_queries::queries::voice_clone_requests::insert_voice_clone_requests_builder::InsertVoiceCloneRequestBuilder; +use http_server_common::response::serialize_as_json_error::serialize_as_json_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::fmt; +use std::sync::Arc; + +// =============== Request =============== + +#[derive(Deserialize)] +pub struct CreateVoiceRequestRequest { + pub idempotency_token: String, + + // Contact + pub email_address: String, + pub discord_username: String, + + // TODO: Make enum. + // Visibility + pub is_for_private_use: bool, + pub is_for_public_use: bool, + + // Use + pub is_for_studio: bool, + pub is_for_twitch_tts: bool, + pub is_for_api_use: bool, + pub is_for_music: bool, + pub is_for_games: bool, + pub is_for_other: bool, + pub optional_notes_on_use: Option, + + // Subject/Ownership + pub is_own_voice: bool, + pub is_third_party_voice: bool, + pub optional_notes_on_subject: Option, + + // Equipment + pub has_clean_audio_recordings: bool, + pub has_good_microphone: bool, + + // Comments + pub optional_questions: Option, + pub optional_extra_comments: Option, +} + +// =============== Success Response =============== + +#[derive(Serialize)] +pub struct CreateVoiceRequestResponse { + pub success: bool, +} + +// =============== Error Response =============== + +#[derive(Debug, Serialize)] +pub enum CreateVoiceRequestError { + BadInput(String), + NotAuthorized, + ServerError, +} + +impl ResponseError for CreateVoiceRequestError { + fn status_code(&self) -> StatusCode { + match *self { + CreateVoiceRequestError::BadInput(_) => StatusCode::BAD_REQUEST, + CreateVoiceRequestError::NotAuthorized => StatusCode::UNAUTHORIZED, + CreateVoiceRequestError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } +} + +// NB: Not using DeriveMore since Clion doesn't understand it. +impl fmt::Display for CreateVoiceRequestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +// =============== Handler =============== + +pub async fn create_voice_clone_request_handler( + http_request: HttpRequest, + 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); + CreateVoiceRequestError::ServerError + })?; + + let creator_ip_address = get_request_ip(&http_request); + + let builder = InsertVoiceCloneRequestBuilder { + uuid_idempotency_token: request.idempotency_token.clone(), + maybe_user_token: maybe_user_session.map(|user| user.user_token.clone()), + email_address: request.email_address.clone(), + discord_username: request.discord_username.clone(), + is_for_private_use: request.is_for_private_use, + is_for_public_use: request.is_for_public_use, + is_for_studio: request.is_for_studio, + is_for_twitch_tts: request.is_for_twitch_tts, + is_for_api_use: request.is_for_api_use, + is_for_music: request.is_for_music, + is_for_games: request.is_for_games, + is_for_other: request.is_for_other, + optional_notes_on_use: request.optional_notes_on_use.clone(), + is_own_voice: request.is_own_voice, + is_third_party_voice: request.is_third_party_voice, + optional_notes_on_subject: request.optional_notes_on_subject.clone(), + has_clean_audio_recordings: request.has_clean_audio_recordings, + has_good_microphone: request.has_good_microphone, + optional_questions: request.optional_questions.clone(), + optional_extra_comments: request.optional_extra_comments.clone(), + ip_address_creation: creator_ip_address, + }; + + builder.insert(&server_state.mysql_pool) + .await + .map_err(|e| { + warn!("creation error: {:?}", e); + CreateVoiceRequestError::ServerError + })?; + + let response = CreateVoiceRequestResponse { + success: true, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| CreateVoiceRequestError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs b/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs new file mode 100644 index 0000000000..8cea07f4c1 --- /dev/null +++ b/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs @@ -0,0 +1,2 @@ +pub mod create_voice_clone_request; +pub mod check_if_voice_clone_request_submitted; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/routes.rs b/crates/service/storyteller_web/src/routes.rs index ac29eebd4e..cd26f9b8f2 100644 --- a/crates/service/storyteller_web/src/routes.rs +++ b/crates/service/storyteller_web/src/routes.rs @@ -79,6 +79,8 @@ use crate::http_server::endpoints::twitch::event_rules::create_event_rule::creat use crate::http_server::endpoints::twitch::event_rules::list_event_rules_for_user::list_twitch_event_rules_for_user_handler; use crate::http_server::endpoints::twitch::event_rules::delete_event_rule::delete_twitch_event_rule_handler; use crate::http_server::endpoints::twitch::event_rules::edit_event_rule::edit_twitch_event_rule_handler; +use crate::http_server::endpoints::voice_clone_requests::create_voice_clone_request::create_voice_clone_request_handler; +use crate::http_server::endpoints::voice_clone_requests::check_if_voice_clone_request_submitted::check_if_voice_clone_request_submitted_handler; pub fn add_routes (app: App) -> App where @@ -97,6 +99,7 @@ pub fn add_routes (app: App) -> App app = add_category_routes(app); /* /category */ app = add_user_profile_routes(app); /* /user */ app = add_api_token_routes(app); /* /api_tokens */ + app = add_voice_clone_request_routes(app); /* /voice_clone_requests */ app = add_twitch_oauth_routes(app); /* /twitch */ // TODO: MAYBE TEMPORARY // ==================== ACCOUNT CREATION / SESSION MANAGEMENT ==================== @@ -571,6 +574,32 @@ fn add_api_token_routes (app: App) -> App ) } +// ==================== API TOKEN ROUTES ==================== + +fn add_voice_clone_request_routes (app: App) -> App + where + B: MessageBody, + T: ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, +{ + app.service(web::scope("/voice_clone_requests") + .service(web::resource("/create") + .route(web::post().to(create_voice_clone_request_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/check") + .route(web::post().to(check_if_voice_clone_request_submitted_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + ) +} + + // ==================== TWITCH ROUTES ==================== // TODO: Maybe move these into an "oauth-gateway" type http service.