From fb8142fb49dce9c0f6e1567c2abe1840dc754e1a Mon Sep 17 00:00:00 2001 From: Paul Sajna Date: Thu, 2 Feb 2023 22:25:00 -0800 Subject: [PATCH] endpoint for indicating why a user is unsubscribing --- .../down.sql | 2 + .../up.sql | 19 ++++ .../insert_unsubscribe_reason.rs | 66 +++++++++++ .../queries/users/user_subscriptions/mod.rs | 1 + .../src/http_server/endpoints/mod.rs | 3 +- .../endpoints/subscriptions/mod.rs | 1 + .../subscriptions/unsubscribe_reason.rs | 104 ++++++++++++++++++ .../service/web/storyteller_web/src/routes.rs | 23 +++- localdev/startup.sh | 4 +- 9 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 _mysql/migrations/2023-02-03-035217_unsubscribe_reason_table/down.sql create mode 100644 _mysql/migrations/2023-02-03-035217_unsubscribe_reason_table/up.sql create mode 100644 crates/lib/database_queries/src/queries/users/user_subscriptions/insert_unsubscribe_reason.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/mod.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/unsubscribe_reason.rs diff --git a/_mysql/migrations/2023-02-03-035217_unsubscribe_reason_table/down.sql b/_mysql/migrations/2023-02-03-035217_unsubscribe_reason_table/down.sql new file mode 100644 index 0000000000..dd1062b2d5 --- /dev/null +++ b/_mysql/migrations/2023-02-03-035217_unsubscribe_reason_table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +drop table unsubscribe_reason; diff --git a/_mysql/migrations/2023-02-03-035217_unsubscribe_reason_table/up.sql b/_mysql/migrations/2023-02-03-035217_unsubscribe_reason_table/up.sql new file mode 100644 index 0000000000..1cda70145b --- /dev/null +++ b/_mysql/migrations/2023-02-03-035217_unsubscribe_reason_table/up.sql @@ -0,0 +1,19 @@ +CREATE TABLE unsubscribe_reason ( + -- Generic primary key + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- User unsubscribing + user_token VARCHAR(32) NOT NULL, + + -- User's feedback/reason for unsubscribing + feedback_reason MEDIUMTEXT, + + -- tracking + ip_address VARCHAR(40) NOT NULL, + + -- Timestamp the feedback was submitted at + unsubscribed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (id) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/crates/lib/database_queries/src/queries/users/user_subscriptions/insert_unsubscribe_reason.rs b/crates/lib/database_queries/src/queries/users/user_subscriptions/insert_unsubscribe_reason.rs new file mode 100644 index 0000000000..559c1fb387 --- /dev/null +++ b/crates/lib/database_queries/src/queries/users/user_subscriptions/insert_unsubscribe_reason.rs @@ -0,0 +1,66 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use log::info; +use log::warn; +use sqlx::MySql; +use sqlx::pool::PoolConnection; + +pub struct UnsubscribeReasonInsertBuilder<'a> { + unsubscribe_reason: &'a str, + user_token: &'a str, + ip_address: &'a str, +} + +impl<'a> UnsubscribeReasonInsertBuilder<'a> { + pub fn new() -> Self { + Self { + unsubscribe_reason: "", + user_token: "", + ip_address: "", + } + } + + pub fn set_unsubscribe_reason(mut self, value: &'a str) -> Self { + self.unsubscribe_reason = value; + self + } + + pub fn set_user_token(mut self, value: &'a str) -> Self { + self.user_token = value; + self + } + + pub fn set_ip_address(mut self, value: &'a str) -> Self { + self.ip_address = value; + self + } + + pub async fn insert(&mut self, mysql_connection: &mut PoolConnection) -> AnyhowResult<()> { + let query = sqlx::query!( + r#" +INSERT INTO unsubscribe_reason +SET + user_token = ?, + feedback_reason = ?, + ip_address = ? + "#, + self.user_token, + self.unsubscribe_reason, + self.ip_address, + ); + + let query_result = query.execute(mysql_connection) + .await; + + let _record_id = match query_result { + Ok(res) => { + res.last_insert_id() + }, + Err(err) => { + return Err(anyhow!("Unsubscribe reason insert DB error: {:?}", err)); + } + }; + + Ok(()) + } +} diff --git a/crates/lib/database_queries/src/queries/users/user_subscriptions/mod.rs b/crates/lib/database_queries/src/queries/users/user_subscriptions/mod.rs index bf24e0b70a..728cd19e33 100644 --- a/crates/lib/database_queries/src/queries/users/user_subscriptions/mod.rs +++ b/crates/lib/database_queries/src/queries/users/user_subscriptions/mod.rs @@ -1,3 +1,4 @@ pub mod get_user_subscription_by_stripe_subscription_id; pub mod list_active_user_subscriptions; pub mod upsert_user_subscription_by_stripe_id; +pub mod insert_unsubscribe_reason; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs index 941d4aec47..312bb45b0f 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs @@ -17,4 +17,5 @@ pub mod user_ratings; pub mod vocoders; pub mod voice_clone_requests; pub mod voice_conversion; -pub mod w2l; \ No newline at end of file +pub mod w2l; +pub mod subscriptions; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/mod.rs new file mode 100644 index 0000000000..ff2380c54d --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/mod.rs @@ -0,0 +1 @@ +pub mod unsubscribe_reason; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/unsubscribe_reason.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/unsubscribe_reason.rs new file mode 100644 index 0000000000..0569b02f97 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/unsubscribe_reason.rs @@ -0,0 +1,104 @@ +use actix_web::http::StatusCode; +use actix_web::{HttpRequest, HttpResponse, ResponseError, web}; +use http_server_common::request::get_request_ip::get_request_ip; +use crate::server_state::ServerState; +use database_queries::queries::users::user_subscriptions::insert_unsubscribe_reason::UnsubscribeReasonInsertBuilder; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use log::{error, info}; +use std::sync::Arc; + +// =============== Request =============== + +#[derive(Deserialize)] +pub struct SetUnsubscribeReasonRequest { + /// The type of the entity being rated. + pub reason: String, +} + +// =============== Success Response =============== + +#[derive(Serialize)] +pub struct SetUnsubscribeReasonResponse { + pub success: bool, +} + +// =============== Error Response =============== + +#[derive(Debug, Serialize)] +pub enum SetUnsubscribeReasonError { + NotAuthorized, + ServerError, +} + +impl ResponseError for SetUnsubscribeReasonError { + fn status_code(&self) -> StatusCode { + match *self { + SetUnsubscribeReasonError::NotAuthorized => StatusCode::UNAUTHORIZED, + SetUnsubscribeReasonError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } +} + +impl std::fmt::Display for SetUnsubscribeReasonError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +// =============== Handler =============== + +pub async fn set_unsubscribe_reason_handler( + http_request: HttpRequest, + request: web::Json, + server_state: web::Data>) -> Result +{ + let mut mysql_connection = server_state.mysql_pool.acquire() + .await + .map_err(|e| { + error!("Could not acquire DB pool: {:?}", e); + SetUnsubscribeReasonError::ServerError + })?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + error!("Session checker error: {:?}", e); + SetUnsubscribeReasonError::ServerError + })?; + + let user_session = match maybe_user_session { + Some(session) => session, + None => { + info!("not logged in"); + return Err(SetUnsubscribeReasonError::NotAuthorized); + } + }; + + let ip_address = get_request_ip(&http_request); + + UnsubscribeReasonInsertBuilder::new() + .set_user_token(&user_session.user_token) + .set_ip_address(&ip_address) + .set_unsubscribe_reason(&request.reason) + .insert(&mut mysql_connection) + .await + .map_err(|_e| SetUnsubscribeReasonError::ServerError)?; + + + let response = SetUnsubscribeReasonResponse { + success: true, + }; + + let body = serde_json::to_string(&response) + .map_err(|_e| SetUnsubscribeReasonError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/service/web/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs index 99ab8fe384..43bc2fc6b1 100644 --- a/crates/service/web/storyteller_web/src/routes.rs +++ b/crates/service/web/storyteller_web/src/routes.rs @@ -102,6 +102,7 @@ use crate::http_server::endpoints::w2l::list_user_w2l_inference_results::list_us use crate::http_server::endpoints::w2l::list_user_w2l_templates::list_user_w2l_templates_handler; use crate::http_server::endpoints::w2l::list_w2l_templates::list_w2l_templates_handler; use crate::http_server::endpoints::w2l::set_w2l_template_mod_approval::set_w2l_template_mod_approval_handler; +use crate::http_server::endpoints::subscriptions::unsubscribe_reason::set_unsubscribe_reason_handler; use users_component::default_routes::add_suggested_api_v1_account_creation_and_session_routes; use users_component::endpoints::edit_profile_handler::edit_profile_handler; use users_component::endpoints::get_profile_handler::get_profile_handler; @@ -134,6 +135,7 @@ pub fn add_routes (app: App) -> App app = add_media_upload_routes(app); /* /v1/media_upload/... */ app = add_trending_routes(app); /* /v1/trending/... */ app = add_user_rating_routes(app); /* /v1/user_rating/... */ + app = add_subscription_routes(app); /* /v1/subscriptions/... */ // From components app = add_suggested_api_v1_account_creation_and_session_routes(app); // /create_account, /session, /login, /logout @@ -936,7 +938,6 @@ fn add_trending_routes (app: App) -> App ) ) } - // ==================== USER RATING ROUTES ==================== fn add_user_rating_routes (app: App) -> App @@ -1026,3 +1027,23 @@ fn add_twitch_routes (app: App) -> App ) ) } + +// ============= SUBSCRIPTION ROUTES =================== +fn add_subscription_routes (app: App) -> App + where + B: MessageBody, + T: ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, +{ + app.service(web::scope("/v1/subscriptions") + .service(web::resource("/unsubscribe_reason") + .route(web::post().to(set_unsubscribe_reason_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + ) +} diff --git a/localdev/startup.sh b/localdev/startup.sh index 295bfde3f3..e44488b6a3 100755 --- a/localdev/startup.sh +++ b/localdev/startup.sh @@ -1,4 +1,6 @@ -#!/bin/sh +#!/bin/bash +set +e +o pipefail diesel migration run --database-url="${MYSQL_URL}" +set -e -o pipefail ./target/x86_64-unknown-linux-musl/release/storyteller-web