mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 17:08:13 +00:00
17 files changed
+676
-120
No files matched your search
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE tts_inference_jobs DROP COLUMN max_duration_seconds;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE tts_inference_jobs
|
||||
ADD COLUMN max_duration_seconds INTEGER NOT NULL DEFAULT 0
|
||||
AFTER is_debug_request;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE w2l_inference_jobs DROP COLUMN max_duration_seconds;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE w2l_inference_jobs
|
||||
ADD COLUMN max_duration_seconds INTEGER NOT NULL DEFAULT 0
|
||||
AFTER creator_set_visibility;
|
||||
@@ -65,6 +65,13 @@ CREATE TABLE tts_inference_jobs (
|
||||
-- If true, the request gets routed to a special "debug" worker.
|
||||
is_debug_request BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- ========== PREMIUM FEATURES METADATA ==========
|
||||
|
||||
-- The maximum duration for generated audio in seconds.
|
||||
-- Zero is implied to be the default value, which is typically 12 seconds.
|
||||
-- A negative value implies "unlimited".
|
||||
max_duration_seconds INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- ========== JOB SYSTEM DETAILS ==========
|
||||
|
||||
-- Jobs begin as "pending", then transition to other states.
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
|
||||
-- with comments attached to the fields for centralized documentation.
|
||||
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
|
||||
-- with comments attached to the fields for centralized documentation.
|
||||
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
CREATE TABLE w2l_inference_jobs (
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Effective "primary key" (PUBLIC)
|
||||
-- This is so the in-progress results can be looked up by the UI.
|
||||
token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Idempotency token from client
|
||||
-- This is so the frontend client doesn't submit duplicate jobs.
|
||||
uuid_idempotency_token VARCHAR(36) NOT NULL,
|
||||
|
||||
-- ========== SUCCESS CASE ==========
|
||||
|
||||
-- If the job completes successfully, this is the result token.
|
||||
-- This is only populated on a successful result.
|
||||
on_success_result_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== INFERENCE DETAILS : FACE TEMPLATE ==========
|
||||
|
||||
-- The W2L template to use
|
||||
-- Can be an image or video.
|
||||
-- This is null if we're using a custom uploaded image.
|
||||
maybe_w2l_template_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- If we're using a custom uploaded image, this will be present.
|
||||
-- NOTE: This may never be supported.
|
||||
maybe_public_image_bucket_location VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- ========== INFERENCE DETAILS : AUDIO SOURCE ==========
|
||||
|
||||
-- If we're using TTS results, this will be present
|
||||
maybe_tts_inference_result_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- If we're using custom uploaded audio, this will be present.
|
||||
maybe_public_audio_bucket_hash VARCHAR(64) DEFAULT NULL,
|
||||
maybe_public_audio_bucket_location VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- The filename that was used at upload time (if available)
|
||||
maybe_original_audio_filename CHAR(255) DEFAULT NULL,
|
||||
|
||||
-- Where the audio file was originally downloaded (if it was downloaded)
|
||||
maybe_original_audio_download_url VARCHAR(512) DEFAULT NULL,
|
||||
|
||||
maybe_audio_mime_type VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- ========== CREATOR DETAILS AND PREFERENCES ==========
|
||||
|
||||
-- Foreign key to user
|
||||
-- If no user is logged in, this is null.
|
||||
maybe_creator_user_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- Based on a cookie sent by the frontend.
|
||||
-- We'll save this even if the user is logged in.
|
||||
maybe_creator_anonymous_visitor_token VARCHAR(32) DEFAULT NULL,
|
||||
|
||||
-- For abuse tracking.
|
||||
-- Wide enough for IPv4/6
|
||||
creator_ip_address VARCHAR(40) NOT NULL,
|
||||
|
||||
-- (THIS MIGHT NOT BE USED)
|
||||
-- NB: DO NOT SORT!
|
||||
-- THIS MUST MATCH THE RESPECTIVE JOBS TABLE.
|
||||
creator_set_visibility ENUM(
|
||||
'public',
|
||||
'hidden',
|
||||
'private'
|
||||
) NOT NULL DEFAULT 'public',
|
||||
|
||||
-- ========== PREMIUM FEATURES METADATA ==========
|
||||
|
||||
-- The maximum duration for generated audio in seconds.
|
||||
-- Zero is implied to be the default value, which is typically 12 seconds.
|
||||
-- A negative value implies "unlimited".
|
||||
max_duration_seconds INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Flags to disable branding
|
||||
disable_end_bump BOOL NOT NULL DEFAULT false,
|
||||
disable_watermark BOOL NOT NULL DEFAULT false,
|
||||
|
||||
-- ========== JOB SYSTEM DETAILS ==========
|
||||
|
||||
-- Jobs begin as "pending", then transition to other states.
|
||||
--
|
||||
-- * Pending = job is ready to go
|
||||
-- * Started = job is running
|
||||
-- * Complete_Success = job is done (success)
|
||||
-- * Complete_Failure = job is done (failure)
|
||||
-- * Attempt_Failed = job failed but may retry.
|
||||
-- * Dead = job failed permanently.
|
||||
--
|
||||
-- Pending -> Started -> Complete_Success
|
||||
-- |-> Complete_Failure
|
||||
-- \-> Attempt_Failed -> Started -> { Complete, Failed, Dead }
|
||||
status ENUM(
|
||||
'pending',
|
||||
'started',
|
||||
'complete_success',
|
||||
'complete_failure',
|
||||
'attempt_failed',
|
||||
'dead') NOT NULL DEFAULT 'pending',
|
||||
|
||||
-- We can track this against a "max_attempt_count"
|
||||
attempt_count INT(3) NOT NULL DEFAULT 0,
|
||||
|
||||
-- If there is a failure, tell the user why.
|
||||
failure_reason VARCHAR(512) DEFAULT NULL,
|
||||
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Failed jobs will set a next attempt time.
|
||||
-- Subsequent tries can increase the timeout.
|
||||
-- Failures because of permissions require human intervention => [retry_at=null].
|
||||
-- Failures because of invalid files are dead => [status=dead].
|
||||
retry_at TIMESTAMP NULL,
|
||||
|
||||
-- INDICES --
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (token),
|
||||
UNIQUE KEY (uuid_idempotency_token),
|
||||
KEY fk_on_success_result_token (on_success_result_token),
|
||||
KEY fk_maybe_w2l_template_token (maybe_w2l_template_token),
|
||||
KEY fk_maybe_tts_inference_result_token (maybe_tts_inference_result_token),
|
||||
KEY fk_maybe_creator_user_token (maybe_creator_user_token),
|
||||
KEY index_status (status),
|
||||
KEY index_creator_ip_address (creator_ip_address)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -1,7 +1,7 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use database_queries::column_types::record_visibility::RecordVisibility;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UserSessionExtended {
|
||||
pub user_token: String,
|
||||
pub user: UserSessionUserDetails,
|
||||
@@ -10,7 +10,7 @@ pub struct UserSessionExtended {
|
||||
pub role: UserSessionRoleAndPermissions,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UserSessionUserDetails {
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
@@ -19,7 +19,7 @@ pub struct UserSessionUserDetails {
|
||||
pub email_gravatar_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UserSessionPremiumPlanInfo {
|
||||
pub maybe_stripe_customer_id: Option<String>,
|
||||
pub maybe_loyalty_program_key: Option<String>,
|
||||
@@ -41,7 +41,16 @@ pub struct UserSessionSubscriptionPlan {
|
||||
pub subscription_expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
impl Default for UserSessionSubscriptionPlan {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
subscription_expires_at: chrono::MIN_DATETIME,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UserSessionPreferences {
|
||||
pub disable_gravatar: bool,
|
||||
pub auto_play_audio_preference: Option<bool>,
|
||||
@@ -50,7 +59,7 @@ pub struct UserSessionPreferences {
|
||||
pub auto_play_video_preference: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UserSessionRoleAndPermissions {
|
||||
// ===== ROLE ===== //
|
||||
pub user_role_slug: String,
|
||||
|
||||
@@ -15,6 +15,10 @@ pub enum RecordVisibility {
|
||||
Private,
|
||||
}
|
||||
|
||||
impl Default for RecordVisibility {
|
||||
fn default() -> Self { Self::Public }
|
||||
}
|
||||
|
||||
impl RecordVisibility {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
|
||||
+26
-1
@@ -21,7 +21,14 @@ pub struct TtsInferenceJobInsertBuilder {
|
||||
creator_set_visibility: Option<String>,
|
||||
is_from_api: bool,
|
||||
is_for_twitch: bool,
|
||||
is_debug_request: bool, // Route the request to a special host with this flag
|
||||
priority_level: u8, // Priority is 0 by default and optionally increases
|
||||
|
||||
/// Premium feature controlling TTS max duration.
|
||||
/// - Negative values imply unlimited duration
|
||||
/// - 0 is "use default" (typically 12 seconds)
|
||||
/// - Positive values are that value in seconds up to some max.
|
||||
max_duration_seconds: i32,
|
||||
}
|
||||
|
||||
impl TtsInferenceJobInsertBuilder {
|
||||
@@ -38,7 +45,9 @@ impl TtsInferenceJobInsertBuilder {
|
||||
creator_set_visibility: None,
|
||||
is_from_api: false,
|
||||
is_for_twitch: false,
|
||||
is_debug_request: false,
|
||||
priority_level: 0,
|
||||
max_duration_seconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +66,9 @@ impl TtsInferenceJobInsertBuilder {
|
||||
creator_set_visibility: Some("hidden".to_string()),
|
||||
is_from_api: false,
|
||||
is_for_twitch: false,
|
||||
is_debug_request: false,
|
||||
priority_level: 0,
|
||||
max_duration_seconds: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,11 +117,21 @@ impl TtsInferenceJobInsertBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_is_debug_request(mut self, value: bool) -> Self {
|
||||
self.is_debug_request = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_priority_level(mut self, value: u8) -> Self {
|
||||
self.priority_level = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_max_duration_seconds(mut self, value: i32) -> Self {
|
||||
self.max_duration_seconds = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn insert(&mut self, mysql_pool: &MySqlPool) -> AnyhowResult<()> {
|
||||
// TODO: These should be custom error types with a custom macro to make this easy.
|
||||
let job_token = self.job_token
|
||||
@@ -150,7 +171,9 @@ SET
|
||||
creator_set_visibility = ?,
|
||||
is_from_api = ?,
|
||||
is_for_twitch = ?,
|
||||
is_debug_request = ?,
|
||||
priority_level = ?,
|
||||
max_duration_seconds = ?,
|
||||
status = "pending"
|
||||
"#,
|
||||
job_token,
|
||||
@@ -162,7 +185,9 @@ SET
|
||||
creator_set_visibility,
|
||||
self.is_from_api,
|
||||
self.is_for_twitch,
|
||||
self.is_debug_request,
|
||||
self.priority_level,
|
||||
self.max_duration_seconds,
|
||||
);
|
||||
|
||||
let query_result = query.execute(mysql_pool)
|
||||
@@ -173,7 +198,7 @@ SET
|
||||
res.last_insert_id()
|
||||
},
|
||||
Err(err) => {
|
||||
return Err(anyhow!("Twitch pubsub bits insert DB error: {:?}", err));
|
||||
return Err(anyhow!("MySQL tts_inference_job record insert error: {:?}", err));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
use std::ops::Deref;
|
||||
use chrono::Utc;
|
||||
use crate::configs::plans::plan::Plan;
|
||||
use crate::configs::plans::plan_list::{DEVELOPMENT_PREMIUM_PLANS, DEVELOPMENT_PREMIUM_PLANS_BY_SLUG, FREE_LOGGED_IN_PLAN, FREE_LOGGED_OUT_PLAN, LOYALTY_PLANS_BY_SLUG, PRODUCTION_PREMIUM_PLANS, PRODUCTION_PREMIUM_PLANS_BY_SLUG};
|
||||
use url_config::server_environment::ServerEnvironment;
|
||||
use users_component::utils::user_session_extended::{UserSessionExtended, UserSessionSubscriptionPlan};
|
||||
|
||||
/// Look up the most appropriate plan for the session.
|
||||
/// This will probably grow to include a lot of factors.
|
||||
pub fn get_correct_plan_for_session(
|
||||
server_environment: ServerEnvironment,
|
||||
maybe_user_session: Option<&UserSessionExtended>,
|
||||
) -> Plan {
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
None => {
|
||||
return FREE_LOGGED_OUT_PLAN.clone();
|
||||
},
|
||||
Some(user_session) => user_session,
|
||||
};
|
||||
|
||||
premium_plan_if_available(server_environment, user_session)
|
||||
.or_else(|| loyalty_plan_if_available(user_session))
|
||||
.unwrap_or(FREE_LOGGED_IN_PLAN.clone())
|
||||
}
|
||||
|
||||
fn premium_plan_if_available(
|
||||
server_environment: ServerEnvironment,
|
||||
user_session: &UserSessionExtended,
|
||||
) -> Option<Plan> {
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
let plan_set = match server_environment {
|
||||
ServerEnvironment::Development => &DEVELOPMENT_PREMIUM_PLANS_BY_SLUG,
|
||||
ServerEnvironment::Production => &PRODUCTION_PREMIUM_PLANS_BY_SLUG,
|
||||
};
|
||||
|
||||
let applicable_plans = user_session.premium.subscription_plans
|
||||
.iter()
|
||||
.filter(|plan| plan.subscription_expires_at.gt(&now))
|
||||
.filter_map(|plan| {
|
||||
plan_set.get(&plan.subscription_product_slug)
|
||||
})
|
||||
.collect::<Vec<&Plan>>();
|
||||
|
||||
// TODO: For now it's only possible for users to have one plan, so we return the first match.
|
||||
// We may need to revisit this if we add additional plans in the future.
|
||||
applicable_plans.get(0).map(|plan| (*plan).clone())
|
||||
}
|
||||
|
||||
fn loyalty_plan_if_available(
|
||||
user_session: &UserSessionExtended,
|
||||
) -> Option<Plan> {
|
||||
user_session.premium.maybe_loyalty_program_key
|
||||
.as_deref()
|
||||
.and_then(|loyalty_key| LOYALTY_PLANS_BY_SLUG.get(loyalty_key))
|
||||
.map(|plan| plan.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::ops::{Add, Sub};
|
||||
use chrono::{Duration, Utc};
|
||||
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
|
||||
use crate::configs::plans::plan_list::{ALL_PLANS_BY_SLUG, FREE_LOGGED_IN_PLAN, FREE_LOGGED_OUT_PLAN};
|
||||
use url_config::server_environment::ServerEnvironment;
|
||||
use users_component::utils::user_session_extended::{UserSessionExtended, UserSessionSubscriptionPlan};
|
||||
|
||||
#[test]
|
||||
fn test_free_logged_out_plan() {
|
||||
let plan = get_correct_plan_for_session(ServerEnvironment::Development, None);
|
||||
assert_eq!(plan, *FREE_LOGGED_OUT_PLAN);
|
||||
|
||||
let plan = get_correct_plan_for_session(ServerEnvironment::Production, None);
|
||||
assert_eq!(plan, *FREE_LOGGED_OUT_PLAN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_free_logged_in_plan() {
|
||||
let mut user_session = UserSessionExtended::default();
|
||||
|
||||
user_session.premium.subscription_plans = vec![];
|
||||
user_session.premium.maybe_loyalty_program_key = None;
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Development,
|
||||
Some(&user_session));
|
||||
|
||||
assert_eq!(plan, *FREE_LOGGED_IN_PLAN);
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Production,
|
||||
Some(&user_session));
|
||||
|
||||
assert_eq!(plan, *FREE_LOGGED_IN_PLAN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loyalty_plan() {
|
||||
let mut user_session = UserSessionExtended::default();
|
||||
|
||||
user_session.premium.subscription_plans = vec![];
|
||||
user_session.premium.maybe_loyalty_program_key = Some("loyalty_plan".to_string());
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Development,
|
||||
Some(&user_session));
|
||||
|
||||
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("loyalty_plan").unwrap());
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Production,
|
||||
Some(&user_session));
|
||||
|
||||
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("loyalty_plan").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_premium_plan_valid_for_production() {
|
||||
let mut user_session = UserSessionExtended::default();
|
||||
|
||||
let future_expiry = Utc::now().add(Duration::days(30));
|
||||
|
||||
user_session.premium.maybe_loyalty_program_key = None;
|
||||
|
||||
user_session.premium.subscription_plans = vec![
|
||||
UserSessionSubscriptionPlan {
|
||||
subscription_namespace: "fakeyou".to_string(),
|
||||
subscription_product_slug: "fakeyou_en_plus".to_string(),
|
||||
subscription_expires_at: future_expiry,
|
||||
}
|
||||
];
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Development,
|
||||
Some(&user_session));
|
||||
|
||||
// NB: The user has a "production" plan, which won't show up in development
|
||||
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("free_logged_in").unwrap());
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Production,
|
||||
Some(&user_session));
|
||||
|
||||
// In production, however, we see the correct plan
|
||||
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("fakeyou_en_plus").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_premium_plan_valid_for_development() {
|
||||
let mut user_session = UserSessionExtended::default();
|
||||
|
||||
let future_expiry = Utc::now().add(Duration::days(30));
|
||||
|
||||
user_session.premium.maybe_loyalty_program_key = None;
|
||||
|
||||
user_session.premium.subscription_plans = vec![
|
||||
UserSessionSubscriptionPlan {
|
||||
subscription_namespace: "fakeyou".to_string(),
|
||||
subscription_product_slug: "development_fakeyou_en_plus".to_string(),
|
||||
subscription_expires_at: future_expiry,
|
||||
}
|
||||
];
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Development,
|
||||
Some(&user_session));
|
||||
|
||||
// NB: In development we see the correct plan
|
||||
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("development_fakeyou_en_plus").unwrap());
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Production,
|
||||
Some(&user_session));
|
||||
|
||||
// NB: The user has a "development" plan, which won't show up in production
|
||||
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("free_logged_in").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expired_plan_no_longer_returned() {
|
||||
let mut user_session = UserSessionExtended::default();
|
||||
|
||||
let already_expired = Utc::now().sub(Duration::days(30));
|
||||
|
||||
user_session.premium.maybe_loyalty_program_key = None;
|
||||
|
||||
user_session.premium.subscription_plans = vec![
|
||||
UserSessionSubscriptionPlan {
|
||||
subscription_namespace: "fakeyou".to_string(),
|
||||
subscription_product_slug: "fakeyou_en_plus".to_string(),
|
||||
subscription_expires_at: already_expired,
|
||||
}
|
||||
];
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Development,
|
||||
Some(&user_session));
|
||||
|
||||
// Premium plan is already expired
|
||||
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("free_logged_in").unwrap());
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Production,
|
||||
Some(&user_session));
|
||||
|
||||
// Premium plan is already expired
|
||||
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("free_logged_in").unwrap());
|
||||
|
||||
// NB: Just to show that it goes back to premium with a expiry in the future
|
||||
let future_expiry = Utc::now().add(Duration::days(30));
|
||||
user_session.premium.subscription_plans.get_mut(0).unwrap().subscription_expires_at = future_expiry;
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Production,
|
||||
Some(&user_session));
|
||||
|
||||
// NB: Now it isn't expired!
|
||||
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("fakeyou_en_plus").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_premium_plan_preferred_over_loyalty_plan() {
|
||||
let mut user_session = UserSessionExtended::default();
|
||||
|
||||
let future_expiry = Utc::now().add(Duration::days(30));
|
||||
|
||||
user_session.premium.maybe_loyalty_program_key = Some("loyalty_plan".to_string());
|
||||
|
||||
user_session.premium.subscription_plans = vec![
|
||||
UserSessionSubscriptionPlan {
|
||||
subscription_namespace: "fakeyou".to_string(),
|
||||
subscription_product_slug: "fakeyou_en_plus".to_string(),
|
||||
subscription_expires_at: future_expiry,
|
||||
}
|
||||
];
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Production,
|
||||
Some(&user_session));
|
||||
|
||||
// In production we see the premium plan
|
||||
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("fakeyou_en_plus").unwrap());
|
||||
|
||||
let plan = get_correct_plan_for_session(
|
||||
ServerEnvironment::Development,
|
||||
Some(&user_session));
|
||||
|
||||
// NB: The user has a "production" plan, and since this is invalid in development,
|
||||
// we see the loyalty plan instead.
|
||||
assert_eq!(&plan, ALL_PLANS_BY_SLUG.get("loyalty_plan").unwrap());
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod get_correct_plan_for_session;
|
||||
pub mod plan;
|
||||
pub mod plan_category;
|
||||
pub mod plan_list;
|
||||
@@ -14,7 +14,7 @@ const W2L_DEFAULT_TIME_LIMIT_SECONDS : i64 = 20;
|
||||
|
||||
/// A Plan is either a free or premium plan.
|
||||
/// Each plan corresponds to a certain level of service.
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub struct Plan {
|
||||
/// Name of the plan in URL-friendly "slug" form (human readable, no spaces, underscores),
|
||||
/// eg. "plan_1", "en_basic", etc.
|
||||
@@ -37,6 +37,9 @@ pub struct Plan {
|
||||
/// Whether this plan is only active in development
|
||||
is_development_plan: bool,
|
||||
|
||||
/// Whether this plan does not exist in Stripe and only exists as defined by this server.
|
||||
is_synthetic_plan: bool,
|
||||
|
||||
// ========== Features for TTS ==========
|
||||
|
||||
tts_base_priority_level: u8,
|
||||
@@ -70,6 +73,7 @@ impl Plan {
|
||||
stripe_product_id: builder.stripe_product_id.clone(),
|
||||
stripe_price_id: builder.stripe_price_id.clone(),
|
||||
is_development_plan: builder.is_development_plan,
|
||||
is_synthetic_plan: builder.is_synthetic_plan,
|
||||
tts_base_priority_level: builder.tts_base_priority_level,
|
||||
tts_max_duration: builder.tts_max_duration,
|
||||
tts_can_generate_mp3: builder.tts_can_generate_mp3,
|
||||
@@ -101,10 +105,22 @@ impl Plan {
|
||||
self.stripe_price_id.as_deref()
|
||||
}
|
||||
|
||||
/// The plan is only valid in "development" Stripe.
|
||||
/// These plans should never be used on the production cluster.
|
||||
pub fn is_development_plan(&self) -> bool {
|
||||
self.is_development_plan
|
||||
}
|
||||
|
||||
/// The plan is a valid Stripe plan in production (not test/development).
|
||||
pub fn is_production_plan(&self) -> bool {
|
||||
!self.is_development_plan
|
||||
}
|
||||
|
||||
/// The plan is "synthetic", ie. does not exist in Stripe.
|
||||
pub fn is_synthetic_plan(&self) -> bool {
|
||||
self.is_synthetic_plan
|
||||
}
|
||||
|
||||
pub fn tts_base_priority_level(&self) -> u8 {
|
||||
self.tts_base_priority_level
|
||||
}
|
||||
@@ -112,6 +128,10 @@ impl Plan {
|
||||
pub fn tts_max_duration(&self) -> Duration {
|
||||
self.tts_max_duration
|
||||
}
|
||||
|
||||
pub fn tts_max_duration_seconds(&self) -> i32 {
|
||||
self.tts_max_duration.num_seconds() as i32
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder pattern for Plans.
|
||||
@@ -123,6 +143,7 @@ pub struct PlanBuilder {
|
||||
stripe_product_id: Option<String>,
|
||||
stripe_price_id: Option<String>,
|
||||
is_development_plan: bool,
|
||||
is_synthetic_plan: bool,
|
||||
|
||||
// ========== Features for TTS ==========
|
||||
|
||||
@@ -158,6 +179,7 @@ impl PlanBuilder {
|
||||
stripe_product_id: None,
|
||||
stripe_price_id: None,
|
||||
is_development_plan: false,
|
||||
is_synthetic_plan: false,
|
||||
|
||||
// TTS
|
||||
tts_base_priority_level : TTS_DEFAULT_PRIORITY_LEVEL,
|
||||
@@ -208,6 +230,11 @@ impl PlanBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_synthetic_plan(mut self, value: bool) -> Self {
|
||||
self.is_synthetic_plan = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tts_base_priority_level(mut self, value: u8) -> Self {
|
||||
self.tts_base_priority_level = value;
|
||||
self
|
||||
|
||||
@@ -9,6 +9,7 @@ use tts_common::priority::{FAKEYOU_ANONYMOUS_PRIORITY_LEVEL, FAKEYOU_LOGGED_IN_P
|
||||
/// This is declared at the top level because it may need to be referenced directly.
|
||||
pub static FREE_LOGGED_OUT_PLAN : Lazy<Plan> = Lazy::new(|| {
|
||||
PlanBuilder::new("free_logged_out")
|
||||
.is_synthetic_plan(true)
|
||||
.plan_category(PlanCategory::Free)
|
||||
.tts_base_priority_level(FAKEYOU_ANONYMOUS_PRIORITY_LEVEL)
|
||||
.build()
|
||||
@@ -18,6 +19,7 @@ pub static FREE_LOGGED_OUT_PLAN : Lazy<Plan> = Lazy::new(|| {
|
||||
/// This is declared at the top level because it may need to be referenced directly.
|
||||
pub static FREE_LOGGED_OUT_FIRST_TRY_PLAN : Lazy<Plan> = Lazy::new(|| {
|
||||
PlanBuilder::new("free_logged_out_first_try")
|
||||
.is_synthetic_plan(true)
|
||||
.plan_category(PlanCategory::Free)
|
||||
.tts_base_priority_level(FAKEYOU_LOGGED_IN_PRIORITY_LEVEL) // NB: Same as logged-in free users.
|
||||
.build()
|
||||
@@ -27,24 +29,28 @@ pub static FREE_LOGGED_OUT_FIRST_TRY_PLAN : Lazy<Plan> = Lazy::new(|| {
|
||||
/// This is declared at the top level because it may need to be referenced directly.
|
||||
pub static FREE_LOGGED_IN_PLAN : Lazy<Plan> = Lazy::new(|| {
|
||||
PlanBuilder::new("free_logged_in")
|
||||
.is_synthetic_plan(true)
|
||||
.plan_category(PlanCategory::Free)
|
||||
.tts_base_priority_level(FAKEYOU_LOGGED_IN_PRIORITY_LEVEL)
|
||||
.build()
|
||||
});
|
||||
|
||||
/// This plan is for users that create models for us but don't pay.
|
||||
/// This is declared at the top level because it may need to be referenced directly.
|
||||
pub static LOYALTY_PLAN : Lazy<Plan> = Lazy::new(|| {
|
||||
PlanBuilder::new("loyalty_plan")
|
||||
/// These plans are for users that create models for us but don't pay.
|
||||
pub static LOYALTY_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
let mut plans = HashSet::new();
|
||||
|
||||
plans.insert(PlanBuilder::new("loyalty_plan")
|
||||
.is_synthetic_plan(true)
|
||||
.plan_category(PlanCategory::LoyaltyReward)
|
||||
.tts_base_priority_level(2)
|
||||
.tts_max_duration_seconds(30)
|
||||
.build()
|
||||
.build());
|
||||
|
||||
plans
|
||||
});
|
||||
|
||||
// TODO: Add by-[stripe-product] lookup HashSets.
|
||||
|
||||
pub static DEVELOPMENT_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
/// These plans do not exist in production!
|
||||
pub static DEVELOPMENT_PREMIUM_PLANS: Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
let mut plans = HashSet::new();
|
||||
|
||||
// ========== English plans ==========
|
||||
@@ -72,8 +78,8 @@ pub static DEVELOPMENT_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
plans
|
||||
});
|
||||
|
||||
/// All premium / paid-for plans in the system.
|
||||
pub static PREMIUM_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
/// All paid-for production premium plans in the system.
|
||||
pub static PRODUCTION_PREMIUM_PLANS: Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
let mut plans = HashSet::new();
|
||||
|
||||
// ========== English plans ==========
|
||||
@@ -111,6 +117,45 @@ pub static PREMIUM_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
plans
|
||||
});
|
||||
|
||||
/// Only loyalty premium plans (non-Stripe, non-PayPal, etc. - internally defined)
|
||||
pub static LOYALTY_PLANS_BY_SLUG : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
|
||||
let mut plans = HashMap::new();
|
||||
|
||||
fn add_plan(plans: &mut HashMap<String, Plan>, plan: &Plan) {
|
||||
plans.insert(plan.plan_slug().to_string(), plan.clone());
|
||||
}
|
||||
|
||||
LOYALTY_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
|
||||
|
||||
plans
|
||||
});
|
||||
|
||||
/// Only development premium plans
|
||||
pub static DEVELOPMENT_PREMIUM_PLANS_BY_SLUG : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
|
||||
let mut plans = HashMap::new();
|
||||
|
||||
fn add_plan(plans: &mut HashMap<String, Plan>, plan: &Plan) {
|
||||
plans.insert(plan.plan_slug().to_string(), plan.clone());
|
||||
}
|
||||
|
||||
DEVELOPMENT_PREMIUM_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
|
||||
|
||||
plans
|
||||
});
|
||||
|
||||
/// Only production premium plans
|
||||
pub static PRODUCTION_PREMIUM_PLANS_BY_SLUG : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
|
||||
let mut plans = HashMap::new();
|
||||
|
||||
fn add_plan(plans: &mut HashMap<String, Plan>, plan: &Plan) {
|
||||
plans.insert(plan.plan_slug().to_string(), plan.clone());
|
||||
}
|
||||
|
||||
PRODUCTION_PREMIUM_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
|
||||
|
||||
plans
|
||||
});
|
||||
|
||||
/// Every single plan in the system: free, paid, loyalty-based, and Stripe development plans.
|
||||
pub static ALL_PLANS_BY_SLUG : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
|
||||
let mut plans = HashMap::new();
|
||||
@@ -122,10 +167,10 @@ pub static ALL_PLANS_BY_SLUG : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
|
||||
add_plan(&mut plans, &FREE_LOGGED_OUT_PLAN);
|
||||
add_plan(&mut plans, &FREE_LOGGED_OUT_FIRST_TRY_PLAN);
|
||||
add_plan(&mut plans, &FREE_LOGGED_IN_PLAN);
|
||||
add_plan(&mut plans, &LOYALTY_PLAN);
|
||||
|
||||
PREMIUM_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
|
||||
DEVELOPMENT_PLANS .iter().for_each(|plan| add_plan(&mut plans, plan));
|
||||
LOYALTY_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
|
||||
PRODUCTION_PREMIUM_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
|
||||
DEVELOPMENT_PREMIUM_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
|
||||
|
||||
plans
|
||||
});
|
||||
@@ -158,13 +203,11 @@ pub static PLANS_BY_STRIPE_PRICE_ID : Lazy<HashMap<String, Plan>> = Lazy::new(||
|
||||
plans
|
||||
});
|
||||
|
||||
// TODO: Add tests to assert our expectations of each plan.
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::configs::plans::plan::Plan;
|
||||
use crate::configs::plans::plan_category::PlanCategory;
|
||||
use crate::configs::plans::plan_list::{ALL_PLANS_BY_SLUG, DEVELOPMENT_PLANS, PLANS_BY_STRIPE_PRICE_ID, PLANS_BY_STRIPE_PRODUCT_ID, PREMIUM_PLANS};
|
||||
use crate::configs::plans::plan_list::{ALL_PLANS_BY_SLUG, DEVELOPMENT_PREMIUM_PLANS, DEVELOPMENT_PREMIUM_PLANS_BY_SLUG, LOYALTY_PLANS, LOYALTY_PLANS_BY_SLUG, PLANS_BY_STRIPE_PRICE_ID, PLANS_BY_STRIPE_PRODUCT_ID, PRODUCTION_PREMIUM_PLANS, PRODUCTION_PREMIUM_PLANS_BY_SLUG};
|
||||
use speculoos::prelude::*;
|
||||
|
||||
// NB: We're being extremely careful in this test and all those that follow, essentially
|
||||
@@ -175,23 +218,41 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_of_production_plans_is_expected() {
|
||||
fn test_number_of_stripe_production_plans_is_expected() {
|
||||
let production_plans = ALL_PLANS_BY_SLUG.values()
|
||||
.filter(|plan| !plan.is_development_plan())
|
||||
.filter(|plan| !plan.is_synthetic_plan()) // NB: Synthetic plans are production!
|
||||
.collect::<Vec<&Plan>>();
|
||||
|
||||
assert_eq!(8, production_plans.len());
|
||||
let expected = 4;
|
||||
|
||||
assert_eq!(expected, production_plans.len());
|
||||
assert_eq!(expected, PRODUCTION_PREMIUM_PLANS.len());
|
||||
assert_eq!(expected, PRODUCTION_PREMIUM_PLANS_BY_SLUG.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_of_development_plans_is_expected() {
|
||||
fn test_number_of_stripe_development_plans_is_expected() {
|
||||
let development_plans = ALL_PLANS_BY_SLUG.values()
|
||||
.filter(|plan| plan.is_development_plan())
|
||||
.collect::<Vec<&Plan>>();
|
||||
|
||||
assert_eq!(2, development_plans.len());
|
||||
let expected = 2;
|
||||
|
||||
assert_eq!(expected, development_plans.len());
|
||||
assert_eq!(expected, DEVELOPMENT_PREMIUM_PLANS.len());
|
||||
assert_eq!(expected, DEVELOPMENT_PREMIUM_PLANS_BY_SLUG.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_of_loyalty_plans_is_expected() {
|
||||
let expected = 1;
|
||||
|
||||
assert_eq!(expected, LOYALTY_PLANS.len());
|
||||
assert_eq!(expected, LOYALTY_PLANS_BY_SLUG.len());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_plans_by_stripe_product_id_are_expected() {
|
||||
let product_ids = PLANS_BY_STRIPE_PRODUCT_ID.keys()
|
||||
@@ -256,65 +317,92 @@ mod test {
|
||||
assert_eq!(120, ALL_PLANS_BY_SLUG.get("fakeyou_en_pro").unwrap().tts_max_duration().num_seconds());
|
||||
}
|
||||
|
||||
// The following tests should not need to change much
|
||||
// =================== THE FOLLOWING TESTS SHOULD NOT NEED TO CHANGE MUCH =================== //
|
||||
|
||||
#[test]
|
||||
fn test_assert_all_premium_plans_are_paid() {
|
||||
let mut test_count = 0;
|
||||
|
||||
PREMIUM_PLANS.iter().for_each(|plan| {
|
||||
fn test_assert_all_production_stripe_plans_are_paid() {
|
||||
PRODUCTION_PREMIUM_PLANS.iter().for_each(|plan| {
|
||||
assert_eq!(plan.plan_category(), PlanCategory::Paid);
|
||||
test_count += 1;
|
||||
});
|
||||
PRODUCTION_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(_, plan)| {
|
||||
assert_eq!(plan.plan_category(), PlanCategory::Paid);
|
||||
});
|
||||
}
|
||||
|
||||
assert!(test_count > 1);
|
||||
#[test]
|
||||
fn test_assert_all_production_stripe_plans_are_non_synthetic() {
|
||||
PRODUCTION_PREMIUM_PLANS.iter().for_each(|plan| {
|
||||
assert!(!plan.is_synthetic_plan());
|
||||
});
|
||||
PRODUCTION_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(_, plan)| {
|
||||
assert!(!plan.is_synthetic_plan());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assert_all_development_stripe_plans_are_marked_development() {
|
||||
let mut test_count = 0;
|
||||
|
||||
DEVELOPMENT_PLANS.iter().for_each(|plan| {
|
||||
DEVELOPMENT_PREMIUM_PLANS.iter().for_each(|plan| {
|
||||
assert!(plan.is_development_plan());
|
||||
});
|
||||
DEVELOPMENT_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(_, plan)| {
|
||||
assert!(plan.is_development_plan());
|
||||
test_count += 1;
|
||||
});
|
||||
|
||||
assert!(test_count > 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assert_correct_key_for_plans_indexed_by_slug() {
|
||||
let mut test_count = 0;
|
||||
fn test_assert_all_development_stripe_plans_are_non_synthetic() {
|
||||
DEVELOPMENT_PREMIUM_PLANS.iter().for_each(|plan| {
|
||||
assert!(!plan.is_synthetic_plan());
|
||||
});
|
||||
DEVELOPMENT_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(_, plan)| {
|
||||
assert!(!plan.is_synthetic_plan());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assert_all_loyalty_plans_are_synthetic() {
|
||||
LOYALTY_PLANS.iter().for_each(|plan| {
|
||||
assert!(plan.is_synthetic_plan());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assert_correct_key_used_for_plans_indexed_by_slug() {
|
||||
ALL_PLANS_BY_SLUG.iter().for_each(|(slug, plan)| {
|
||||
assert_eq!(slug, plan.plan_slug());
|
||||
test_count += 1;
|
||||
});
|
||||
|
||||
assert!(test_count > 1);
|
||||
PRODUCTION_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(slug, plan)| {
|
||||
assert_eq!(slug, plan.plan_slug());
|
||||
});
|
||||
DEVELOPMENT_PREMIUM_PLANS_BY_SLUG.iter().for_each(|(slug, plan)| {
|
||||
assert_eq!(slug, plan.plan_slug());
|
||||
});
|
||||
LOYALTY_PLANS_BY_SLUG.iter().for_each(|(slug, plan)| {
|
||||
assert_eq!(slug, plan.plan_slug());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assert_correct_key_for_plans_indexed_by_product_id() {
|
||||
let mut test_count = 0;
|
||||
|
||||
PLANS_BY_STRIPE_PRODUCT_ID.iter().for_each(|(product_id, plan)| {
|
||||
assert_eq!(product_id, plan.stripe_product_id().unwrap());
|
||||
test_count += 1;
|
||||
});
|
||||
|
||||
assert!(test_count > 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_assert_correct_key_for_plans_indexed_by_price_id() {
|
||||
let mut test_count = 0;
|
||||
|
||||
PLANS_BY_STRIPE_PRICE_ID.iter().for_each(|(price_id, plan)| {
|
||||
assert_eq!(price_id, plan.stripe_price_id().unwrap());
|
||||
test_count += 1;
|
||||
});
|
||||
}
|
||||
|
||||
assert!(test_count > 1);
|
||||
#[test]
|
||||
fn test_assert_stripe_plans_are_non_synthetic() {
|
||||
PLANS_BY_STRIPE_PRICE_ID.iter().for_each(|(price_id, plan)| {
|
||||
assert!(!plan.is_synthetic_plan());
|
||||
});
|
||||
PLANS_BY_STRIPE_PRODUCT_ID.iter().for_each(|(price_id, plan)| {
|
||||
assert!(!plan.is_synthetic_plan());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::server_state::ServerState;
|
||||
use crate::validations::model_uploads::validate_model_title;
|
||||
use database_queries::column_types::record_visibility::RecordVisibility;
|
||||
use database_queries::queries::tts::tts_inference_jobs::insert_tts_inference_job::TtsInferenceJobInsertBuilder;
|
||||
use database_queries::tokens::Tokens;
|
||||
use http_server_common::request::get_request_api_token::get_request_api_token;
|
||||
use http_server_common::request::get_request_header_optional::get_request_header_optional;
|
||||
@@ -18,13 +19,11 @@ use log::{info, warn, log};
|
||||
use r2d2_redis::redis::Commands;
|
||||
use redis_common::redis_keys::RedisKeys;
|
||||
use regex::Regex;
|
||||
use sqlx::error::DatabaseError;
|
||||
use sqlx::error::Error::Database;
|
||||
use sqlx::mysql::MySqlDatabaseError;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use tts_common::priority::{FAKEYOU_LOGGED_IN_PRIORITY_LEVEL, FAKEYOU_ANONYMOUS_PRIORITY_LEVEL, FAKEYOU_INVESTOR_PRIORITY_LEVEL, FAKEYOU_DEFAULT_VALID_API_TOKEN_PRIORITY_LEVEL};
|
||||
use user_input_common::check_for_slurs::contains_slurs;
|
||||
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
|
||||
|
||||
// TODO: Temporary for investor demo
|
||||
const STORYTELLER_DEMO_COOKIE_NAME : &'static str = "storyteller_demo";
|
||||
@@ -92,15 +91,23 @@ pub async fn infer_tts_handler(
|
||||
{
|
||||
let mut is_from_api = false;
|
||||
let mut maybe_user_token : Option<String> = None;
|
||||
let mut priority_level = FAKEYOU_ANONYMOUS_PRIORITY_LEVEL;
|
||||
let mut priority_level ;
|
||||
let mut use_high_priority_rate_limiter = false; // NB: Careful!
|
||||
let mut disable_rate_limiter = false; // NB: Careful!
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("MySql pool error: {:?}", err);
|
||||
InferTtsError::ServerError
|
||||
})?;
|
||||
|
||||
// ==================== USER SESSION ==================== //
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
@@ -109,9 +116,15 @@ pub async fn infer_tts_handler(
|
||||
|
||||
if let Some(user_session) = maybe_user_session.as_ref() {
|
||||
maybe_user_token = Some(user_session.user_token.to_string());
|
||||
priority_level = FAKEYOU_LOGGED_IN_PRIORITY_LEVEL; // Give logged in users execution priority.
|
||||
}
|
||||
|
||||
// TODO: Plan should handle "first anonymous use" and "investor" cases.
|
||||
let plan = get_correct_plan_for_session(
|
||||
server_state.server_environment,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
priority_level = plan.tts_base_priority_level();
|
||||
|
||||
// ==================== API TOKENS ==================== //
|
||||
|
||||
if let Some(api_token) = get_request_api_token(&http_request) {
|
||||
@@ -171,7 +184,7 @@ pub async fn infer_tts_handler(
|
||||
let mut rate_limiter = match maybe_user_session {
|
||||
None => &server_state.redis_rate_limiters.logged_out,
|
||||
Some(ref user) => {
|
||||
if user.is_banned {
|
||||
if user.role.is_banned {
|
||||
return Err(InferTtsError::NotAuthorized);
|
||||
}
|
||||
&server_state.redis_rate_limiters.logged_in
|
||||
@@ -227,7 +240,7 @@ pub async fn infer_tts_handler(
|
||||
|
||||
let maybe_user_preferred_visibility : Option<RecordVisibility> = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|user_session| user_session.preferred_tts_result_visibility);
|
||||
.map(|user_session| user_session.preferences.preferred_tts_result_visibility);
|
||||
|
||||
let set_visibility = request.creator_set_visibility
|
||||
.or(maybe_user_preferred_visibility)
|
||||
@@ -240,69 +253,30 @@ pub async fn infer_tts_handler(
|
||||
InferTtsError::ServerError
|
||||
})?;
|
||||
|
||||
info!("Creating w2l inference job record...");
|
||||
info!("Creating tts inference job record...");
|
||||
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO tts_inference_jobs
|
||||
SET
|
||||
token = ?,
|
||||
uuid_idempotency_token = ?,
|
||||
let query_result = TtsInferenceJobInsertBuilder::new_for_fakeyou_request()
|
||||
.set_job_token(&job_token)
|
||||
.set_uuid_idempotency_token(&request.uuid_idempotency_token)
|
||||
.set_model_token(&model_token)
|
||||
.set_raw_inference_text(&inference_text)
|
||||
.set_maybe_creator_user_token(maybe_user_token.as_deref())
|
||||
.set_creator_ip_address(&ip_address)
|
||||
.set_creator_set_visibility(set_visibility.to_str())
|
||||
.set_priority_level(priority_level)
|
||||
.set_max_duration_seconds(plan.tts_max_duration_seconds())
|
||||
.set_is_from_api(is_from_api)
|
||||
.set_is_debug_request(is_debug_request)
|
||||
.insert(&server_state.mysql_pool)
|
||||
.await;
|
||||
|
||||
model_token = ?,
|
||||
raw_inference_text = ?,
|
||||
maybe_creator_user_token = ?,
|
||||
creator_ip_address = ?,
|
||||
creator_set_visibility = ?,
|
||||
priority_level = ?,
|
||||
is_from_api = ?,
|
||||
is_debug_request = ?,
|
||||
status = "pending"
|
||||
"#,
|
||||
&job_token,
|
||||
request.uuid_idempotency_token.clone(),
|
||||
model_token,
|
||||
inference_text,
|
||||
maybe_user_token,
|
||||
ip_address,
|
||||
set_visibility.to_str(),
|
||||
priority_level,
|
||||
is_from_api,
|
||||
is_debug_request,
|
||||
)
|
||||
.execute(&server_state.mysql_pool)
|
||||
.await;
|
||||
|
||||
let record_id = match query_result {
|
||||
Ok(res) => {
|
||||
res.last_insert_id()
|
||||
},
|
||||
match query_result {
|
||||
Ok(_) => {},
|
||||
Err(err) => {
|
||||
warn!("New w2l inference job creation DB error: {:?}", err);
|
||||
|
||||
// NB: SQLSTATE[23000]: Integrity constraint violation
|
||||
// NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access)
|
||||
match err {
|
||||
Database(err) => {
|
||||
let maybe_code = err.code().map(|c| c.into_owned());
|
||||
/*match maybe_code.as_deref() {
|
||||
Some("23000") => {
|
||||
if err.message().contains("username") {
|
||||
return Err(UsernameTaken);
|
||||
} else if err.message().contains("email_address") {
|
||||
return Err(EmailTaken);
|
||||
}
|
||||
}
|
||||
_ => {},
|
||||
}*/
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
warn!("New tts inference job creation DB error: {:?}", err);
|
||||
return Err(InferTtsError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
info!("new w2l inference job id: {}", record_id);
|
||||
}
|
||||
|
||||
server_state.firehose_publisher.enqueue_tts_inference(maybe_user_token.as_deref(), &job_token, &model_token)
|
||||
.await
|
||||
|
||||
@@ -349,6 +349,7 @@ async fn main() -> AnyhowResult<()> {
|
||||
client: stripe_client,
|
||||
},
|
||||
hostname: server_hostname,
|
||||
server_environment,
|
||||
third_party_url_redirector,
|
||||
health_check_status,
|
||||
mysql_pool: pool,
|
||||
|
||||
@@ -14,6 +14,7 @@ use memory_caching::single_item_ttl_cache::SingleItemTtlCache;
|
||||
use r2d2_redis::{r2d2, RedisConnectionManager};
|
||||
use sqlx::MySqlPool;
|
||||
use storage_buckets_common::bucket_client::BucketClient;
|
||||
use url_config::server_environment::ServerEnvironment;
|
||||
use url_config::third_party_url_redirector::ThirdPartyUrlRedirector;
|
||||
use users_component::utils::session_checker::SessionChecker;
|
||||
use users_component::utils::session_cookie_manager::SessionCookieManager;
|
||||
@@ -27,6 +28,9 @@ pub struct ServerState {
|
||||
|
||||
pub hostname: String,
|
||||
|
||||
/// Knowing if we're in production will allow us to turn off development-only functionalities.
|
||||
pub server_environment: ServerEnvironment,
|
||||
|
||||
pub third_party_url_redirector: ThirdPartyUrlRedirector,
|
||||
|
||||
pub health_check_status: HealthCheckStatus,
|
||||
|
||||
Reference in new issue
Block a user