mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 15:55:41 -05:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9af5835c7 | ||
|
|
32deedc6f8 | ||
|
|
22ba7338cd | ||
|
|
badabe57eb | ||
|
|
2e62eef85b | ||
|
|
fd9f002615 | ||
|
|
a0e50067bb |
Generated
-1
@@ -6089,7 +6089,6 @@ dependencies = [
|
||||
name = "jwt_signer"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"errors",
|
||||
"hmac 0.10.1",
|
||||
"jwt",
|
||||
"sha2 0.9.9",
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::PlanFalFluxPro11;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_pro_1p1(
|
||||
plan: &PlanFalFluxPro11<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04 per megapixel, billed by rounding up to nearest megapixel.
|
||||
// Default image sizes are ~1MP (1024x1024), so ~$0.04/image = 4 cents.
|
||||
let cost_per_image: u64 = 4;
|
||||
let num_images = plan.num_images.as_u64();
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_pro_1p1_ultra(
|
||||
plan: &PlanFalFluxPro11Ultra<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.06 per image.
|
||||
let cost_per_image: u64 = 6;
|
||||
let num_images = plan.num_images.as_u64();
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::{
|
||||
FalGptImage1p5ImageSize, FalGptImage1p5Quality, PlanFalGptImage1p5,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_gpt_image_1p5(
|
||||
plan: &PlanFalGptImage1p5<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Per fal docs:
|
||||
// Low: $0.009 (1024x1024) / $0.013 (other)
|
||||
// Medium: $0.034 (1024x1024) / $0.050–0.051 (other)
|
||||
// High: $0.133 (1024x1024) / $0.199–0.200 (other)
|
||||
// Round up to whole cents.
|
||||
let is_square = matches!(plan.image_size, None | Some(FalGptImage1p5ImageSize::Square));
|
||||
let cost_per_image: u64 = match (plan.quality, is_square) {
|
||||
(FalGptImage1p5Quality::Low, true) => 1,
|
||||
(FalGptImage1p5Quality::Low, false) => 2,
|
||||
(FalGptImage1p5Quality::Medium, true) => 4,
|
||||
(FalGptImage1p5Quality::Medium, false) => 6,
|
||||
(FalGptImage1p5Quality::High, true) => 14,
|
||||
(FalGptImage1p5Quality::High, false) => 20,
|
||||
};
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_4(
|
||||
plan: &PlanFalSeedream4<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.03 per image.
|
||||
let cost_per_image: u64 = 3;
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_4p5(
|
||||
plan: &PlanFalSeedream4p5<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04 per image.
|
||||
let cost_per_image: u64 = 4;
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_5_lite(
|
||||
plan: &PlanFalSeedream5Lite<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.035 per image — round up to 4 cents.
|
||||
let cost_per_image: u64 = 4;
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
+6
@@ -1,5 +1,11 @@
|
||||
pub(crate) mod estimate_image_cost_fal_flux_1_dev;
|
||||
pub(crate) mod estimate_image_cost_fal_flux_1_schnell;
|
||||
pub(crate) mod estimate_image_cost_fal_flux_pro_1p1;
|
||||
pub(crate) mod estimate_image_cost_fal_flux_pro_1p1_ultra;
|
||||
pub(crate) mod estimate_image_cost_fal_gpt_image_1p5;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_4;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_4p5;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_5_lite;
|
||||
pub(crate) mod estimate_image_cost_fal_nano_banana;
|
||||
pub(crate) mod estimate_image_cost_fal_nano_banana_2;
|
||||
pub(crate) mod estimate_image_cost_fal_nano_banana_pro;
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::PlanFalFluxPro11;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
|
||||
enqueue_flux_pro_11_text_to_image_webhook, FluxPro11Args,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_pro_1p1(
|
||||
plan: &PlanFalFluxPro11<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let args = FluxPro11Args {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: plan.num_images.to_fal(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
|
||||
let webhook_response = enqueue_flux_pro_11_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
|
||||
enqueue_flux_pro_11_ultra_text_to_image_webhook, FluxPro11UltraArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_pro_1p1_ultra(
|
||||
plan: &PlanFalFluxPro11Ultra<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let args = FluxPro11UltraArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: plan.num_images.to_fal(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
|
||||
let webhook_response = enqueue_flux_pro_11_ultra_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::PlanFalGptImage1p5;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{
|
||||
enqueue_gpt_image_1p5_image_edit_webhook, EnqueueGptImage1p5EditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{
|
||||
enqueue_gpt_image_1p5_text_to_image_webhook, EnqueueGptImage1p5TextToImageArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_gpt_image_1p5(
|
||||
plan: &PlanFalGptImage1p5<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueGptImage1p5TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
background: None,
|
||||
quality: Some(plan.quality.to_t2i()),
|
||||
output_format: None,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gpt_image_1p5_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueGptImage1p5EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: plan.num_images.to_edit(),
|
||||
mask_image_url: None,
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
background: None,
|
||||
quality: Some(plan.quality.to_edit()),
|
||||
input_fidelity: None,
|
||||
output_format: None,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gpt_image_1p5_image_edit_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{
|
||||
enqueue_bytedance_seedream_v4_edit_image_webhook, EnqueueBytedanceSeedreamV4EditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{
|
||||
enqueue_bytedance_seedream_v4_text_to_image_webhook, EnqueueBytedanceSeedreamV4TextToImageArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_seedream_4(
|
||||
plan: &PlanFalSeedream4<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueBytedanceSeedreamV4TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v4_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueBytedanceSeedreamV4EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v4_edit_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{
|
||||
enqueue_bytedance_seedream_v4p5_edit_image_webhook, EnqueueBytedanceSeedreamV4p5EditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{
|
||||
enqueue_bytedance_seedream_v4p5_text_to_image_webhook, EnqueueBytedanceSeedreamV4p5TextToImageArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_seedream_4p5(
|
||||
plan: &PlanFalSeedream4p5<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v4p5_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueBytedanceSeedreamV4p5EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v4p5_edit_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::{
|
||||
enqueue_bytedance_seedream_v5_lite_edit_image_webhook, EnqueueBytedanceSeedreamV5LiteEditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{
|
||||
enqueue_bytedance_seedream_v5_lite_text_to_image_webhook, EnqueueBytedanceSeedreamV5LiteTextToImageArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_seedream_5_lite(
|
||||
plan: &PlanFalSeedream5Lite<'_>,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueBytedanceSeedreamV5LiteTextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v5_lite_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueBytedanceSeedreamV5LiteEditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_bytedance_seedream_v5_lite_edit_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
+6
@@ -1,5 +1,11 @@
|
||||
pub mod generate_image_fal_flux_1_dev;
|
||||
pub mod generate_image_fal_flux_1_schnell;
|
||||
pub mod generate_image_fal_flux_pro_1p1;
|
||||
pub mod generate_image_fal_flux_pro_1p1_ultra;
|
||||
pub mod generate_image_fal_gpt_image_1p5;
|
||||
pub mod generate_image_fal_seedream_4;
|
||||
pub mod generate_image_fal_seedream_4p5;
|
||||
pub mod generate_image_fal_seedream_5_lite;
|
||||
pub mod generate_image_fal_nano_banana;
|
||||
pub mod generate_image_fal_nano_banana_2;
|
||||
pub mod generate_image_fal_nano_banana_pro;
|
||||
|
||||
+12
@@ -23,6 +23,12 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::plan_generate_image_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::plan_generate_image_fal_flux_1_dev;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::plan_generate_image_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::plan_generate_image_fal_flux_pro_1p1;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::plan_generate_image_fal_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::plan_generate_image_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::plan_generate_image_fal_seedream_4;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::plan_generate_image_fal_seedream_4p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::plan_generate_image_fal_seedream_5_lite;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::plan_generate_image_fal_nano_banana;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::plan_generate_image_fal_nano_banana_2;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro;
|
||||
@@ -106,6 +112,12 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
match self.model {
|
||||
CommonImageModel::Flux1Dev => plan_generate_image_fal_flux_1_dev(self),
|
||||
CommonImageModel::Flux1Schnell => plan_generate_image_fal_flux_1_schnell(self),
|
||||
CommonImageModel::FluxPro11 => plan_generate_image_fal_flux_pro_1p1(self),
|
||||
CommonImageModel::FluxPro11Ultra => plan_generate_image_fal_flux_pro_1p1_ultra(self),
|
||||
CommonImageModel::GptImage1p5 => plan_generate_image_fal_gpt_image_1p5(self),
|
||||
CommonImageModel::Seedream4 => plan_generate_image_fal_seedream_4(self),
|
||||
CommonImageModel::Seedream4p5 => plan_generate_image_fal_seedream_4p5(self),
|
||||
CommonImageModel::Seedream5Lite => plan_generate_image_fal_seedream_5_lite(self),
|
||||
CommonImageModel::NanoBanana => plan_generate_image_fal_nano_banana(self),
|
||||
CommonImageModel::NanoBanana2 => plan_generate_image_fal_nano_banana_2(self),
|
||||
CommonImageModel::NanoBananaPro => plan_generate_image_fal_nano_banana_pro(self),
|
||||
|
||||
+66
@@ -15,6 +15,12 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_2_lora_angles::estimate_image_cost_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_1_dev::estimate_image_cost_fal_flux_1_dev;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_1_schnell::estimate_image_cost_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_pro_1p1::estimate_image_cost_fal_flux_pro_1p1;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_pro_1p1_ultra::estimate_image_cost_fal_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_gpt_image_1p5::estimate_image_cost_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_4::estimate_image_cost_fal_seedream_4;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_4p5::estimate_image_cost_fal_seedream_4p5;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_5_lite::estimate_image_cost_fal_seedream_5_lite;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana::estimate_image_cost_fal_nano_banana;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_2::estimate_image_cost_fal_nano_banana_2;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_pro::estimate_image_cost_fal_nano_banana_pro;
|
||||
@@ -33,6 +39,12 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_2_lora_angles::execute_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_1_dev::execute_fal_flux_1_dev;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_1_schnell::execute_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_pro_1p1::execute_fal_flux_pro_1p1;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_pro_1p1_ultra::execute_fal_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_gpt_image_1p5::execute_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_4::execute_fal_seedream_4;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_4p5::execute_fal_seedream_4p5;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_5_lite::execute_fal_seedream_5_lite;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana::execute_fal_nano_banana;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_2::execute_fal_nano_banana_2;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_pro::execute_fal_nano_banana_pro;
|
||||
@@ -53,6 +65,12 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::PlanFalFluxPro11;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::PlanFalGptImage1p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::PlanFalNanoBanana;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::PlanFalNanaBanana2;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::PlanFalNanaBananaPro;
|
||||
@@ -74,6 +92,12 @@ pub enum ImageGenerationPlan<'a> {
|
||||
ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles<'a>),
|
||||
FalFlux1Dev(PlanFalFlux1Dev<'a>),
|
||||
FalFlux1Schnell(PlanFalFlux1Schnell<'a>),
|
||||
FalFluxPro11(PlanFalFluxPro11<'a>),
|
||||
FalFluxPro11Ultra(PlanFalFluxPro11Ultra<'a>),
|
||||
FalGptImage1p5(PlanFalGptImage1p5<'a>),
|
||||
FalSeedream4(PlanFalSeedream4<'a>),
|
||||
FalSeedream4p5(PlanFalSeedream4p5<'a>),
|
||||
FalSeedream5Lite(PlanFalSeedream5Lite<'a>),
|
||||
FalNanoBanana(PlanFalNanoBanana<'a>),
|
||||
FalNanaBanana2(PlanFalNanaBanana2<'a>),
|
||||
FalNanaBananaPro(PlanFalNanaBananaPro<'a>),
|
||||
@@ -145,6 +169,30 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_flux_1_schnell(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalFluxPro11(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_flux_pro_1p1(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalFluxPro11Ultra(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_flux_pro_1p1_ultra(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalGptImage1p5(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_gpt_image_1p5(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream4(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_seedream_4(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream4p5(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_seedream_4p5(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream5Lite(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_seedream_5_lite(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalNanoBanana(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_nano_banana(plan, fal_client).await
|
||||
@@ -207,6 +255,24 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
ImageGenerationPlan::FalFlux1Schnell(plan) => {
|
||||
estimate_image_cost_fal_flux_1_schnell(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalFluxPro11(plan) => {
|
||||
estimate_image_cost_fal_flux_pro_1p1(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalFluxPro11Ultra(plan) => {
|
||||
estimate_image_cost_fal_flux_pro_1p1_ultra(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalGptImage1p5(plan) => {
|
||||
estimate_image_cost_fal_gpt_image_1p5(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream4(plan) => {
|
||||
estimate_image_cost_fal_seedream_4(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream4p5(plan) => {
|
||||
estimate_image_cost_fal_seedream_4p5(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream5Lite(plan) => {
|
||||
estimate_image_cost_fal_seedream_5_lite(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalNanoBanana(plan) => {
|
||||
estimate_image_cost_fal_nano_banana(plan)
|
||||
}
|
||||
|
||||
+6
@@ -1,5 +1,11 @@
|
||||
pub mod plan_generate_image_fal_flux_1_dev;
|
||||
pub mod plan_generate_image_fal_flux_1_schnell;
|
||||
pub mod plan_generate_image_fal_flux_pro_1p1;
|
||||
pub mod plan_generate_image_fal_flux_pro_1p1_ultra;
|
||||
pub mod plan_generate_image_fal_gpt_image_1p5;
|
||||
pub mod plan_generate_image_fal_seedream_4;
|
||||
pub mod plan_generate_image_fal_seedream_4p5;
|
||||
pub mod plan_generate_image_fal_seedream_5_lite;
|
||||
pub mod plan_generate_image_fal_nano_banana;
|
||||
pub mod plan_generate_image_fal_nano_banana_2;
|
||||
pub mod plan_generate_image_fal_nano_banana_pro;
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
|
||||
FluxPro11AspectRatio, FluxPro11NumImages,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalFluxPro11NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalFluxPro11<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub aspect_ratio: FluxPro11AspectRatio,
|
||||
pub num_images: FalFluxPro11NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_flux_pro_1p1<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
if request.image_inputs.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Flux Pro 1.1 is text-to-image only".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalFluxPro11(PlanFalFluxPro11 {
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FluxPro11AspectRatio, ArtcraftRouterError> {
|
||||
use FluxPro11AspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Ar::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
|
||||
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineBySixteen),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Ar::Square),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalFluxPro11NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalFluxPro11NumImages::One),
|
||||
2 => Ok(FalFluxPro11NumImages::Two),
|
||||
3 => Ok(FalFluxPro11NumImages::Three),
|
||||
4 => Ok(FalFluxPro11NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalFluxPro11NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalFluxPro11NumImages {
|
||||
pub fn to_fal(self) -> FluxPro11NumImages {
|
||||
match self {
|
||||
Self::One => FluxPro11NumImages::One,
|
||||
Self::Two => FluxPro11NumImages::Two,
|
||||
Self::Three => FluxPro11NumImages::Three,
|
||||
Self::Four => FluxPro11NumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
|
||||
FluxPro11UltraAspectRatio, FluxPro11UltraNumImages,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalFluxPro11UltraNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalFluxPro11Ultra<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub aspect_ratio: FluxPro11UltraAspectRatio,
|
||||
pub num_images: FalFluxPro11UltraNumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_flux_pro_1p1_ultra<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
if request.image_inputs.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Flux Pro 1.1 Ultra is text-to-image only".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalFluxPro11Ultra(PlanFalFluxPro11Ultra {
|
||||
prompt: request.prompt,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FluxPro11UltraAspectRatio, ArtcraftRouterError> {
|
||||
use FluxPro11UltraAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeThreeByTwo),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeTwentyOneByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitTwoByThree),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
|
||||
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineByTwentyOne),
|
||||
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Ar::PortraitThreeByFour),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Ar::Square),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalFluxPro11UltraNumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalFluxPro11UltraNumImages::One),
|
||||
2 => Ok(FalFluxPro11UltraNumImages::Two),
|
||||
3 => Ok(FalFluxPro11UltraNumImages::Three),
|
||||
4 => Ok(FalFluxPro11UltraNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalFluxPro11UltraNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalFluxPro11UltraNumImages {
|
||||
pub fn to_fal(self) -> FluxPro11UltraNumImages {
|
||||
match self {
|
||||
Self::One => FluxPro11UltraNumImages::One,
|
||||
Self::Two => FluxPro11UltraNumImages::Two,
|
||||
Self::Three => FluxPro11UltraNumImages::Three,
|
||||
Self::Four => FluxPro11UltraNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{
|
||||
EnqueueGptImage1p5EditImageNumImages, EnqueueGptImage1p5EditImageQuality,
|
||||
EnqueueGptImage1p5EditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{
|
||||
EnqueueGptImage1p5TextToImageNumImages, EnqueueGptImage1p5TextToImageQuality,
|
||||
EnqueueGptImage1p5TextToImageSize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage1p5NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
/// Quality is shared between t2i and edit (Low / Medium / High).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage1p5Quality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
/// GPT Image 1.5 supports three image sizes: 1024x1024, 1536x1024, 1024x1536.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage1p5ImageSize {
|
||||
Square,
|
||||
Wide,
|
||||
Tall,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalGptImage1p5<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub image_urls: Vec<String>,
|
||||
pub image_size: Option<FalGptImage1p5ImageSize>,
|
||||
pub quality: FalGptImage1p5Quality,
|
||||
pub num_images: FalGptImage1p5NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_gpt_image_1p5<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio);
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalGptImage1p5(PlanFalGptImage1p5 {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
image_size,
|
||||
quality: FalGptImage1p5Quality::Medium,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef<'_>>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<FalGptImage1p5ImageSize> {
|
||||
use FalGptImage1p5ImageSize as S;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => None,
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Some(S::Square),
|
||||
|
||||
Some(CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(CommonAspectRatio::WideFourByThree)
|
||||
| Some(CommonAspectRatio::WideFiveByFour)
|
||||
| Some(CommonAspectRatio::WideSixteenByNine)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(CommonAspectRatio::Wide) => Some(S::Wide),
|
||||
|
||||
Some(CommonAspectRatio::TallTwoByThree)
|
||||
| Some(CommonAspectRatio::TallThreeByFour)
|
||||
| Some(CommonAspectRatio::TallFourByFive)
|
||||
| Some(CommonAspectRatio::TallNineBySixteen)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne)
|
||||
| Some(CommonAspectRatio::Tall) => Some(S::Tall),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalGptImage1p5NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalGptImage1p5NumImages::One),
|
||||
2 => Ok(FalGptImage1p5NumImages::Two),
|
||||
3 => Ok(FalGptImage1p5NumImages::Three),
|
||||
4 => Ok(FalGptImage1p5NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalGptImage1p5NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalGptImage1p5NumImages {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage1p5TextToImageNumImages {
|
||||
use EnqueueGptImage1p5TextToImageNumImages as T;
|
||||
match self {
|
||||
Self::One => T::One,
|
||||
Self::Two => T::Two,
|
||||
Self::Three => T::Three,
|
||||
Self::Four => T::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage1p5EditImageNumImages {
|
||||
use EnqueueGptImage1p5EditImageNumImages as E;
|
||||
match self {
|
||||
Self::One => E::One,
|
||||
Self::Two => E::Two,
|
||||
Self::Three => E::Three,
|
||||
Self::Four => E::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalGptImage1p5Quality {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage1p5TextToImageQuality {
|
||||
use EnqueueGptImage1p5TextToImageQuality as T;
|
||||
match self {
|
||||
Self::Low => T::Low,
|
||||
Self::Medium => T::Medium,
|
||||
Self::High => T::High,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage1p5EditImageQuality {
|
||||
use EnqueueGptImage1p5EditImageQuality as E;
|
||||
match self {
|
||||
Self::Low => E::Low,
|
||||
Self::Medium => E::Medium,
|
||||
Self::High => E::High,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalGptImage1p5ImageSize {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage1p5TextToImageSize {
|
||||
use EnqueueGptImage1p5TextToImageSize as T;
|
||||
match self {
|
||||
Self::Square => T::Square,
|
||||
Self::Wide => T::Wide,
|
||||
Self::Tall => T::Tall,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage1p5EditImageSize {
|
||||
use EnqueueGptImage1p5EditImageSize as E;
|
||||
match self {
|
||||
Self::Square => E::Square,
|
||||
Self::Wide => E::Wide,
|
||||
Self::Tall => E::Tall,
|
||||
}
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV4EditImageNumImages, EnqueueBytedanceSeedreamV4EditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV4TextToImageNumImages, EnqueueBytedanceSeedreamV4TextToImageSize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream4NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
/// Shared image-size shape between Seedream V4 t2i and edit (variants are identical).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream4ImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
Auto,
|
||||
Auto2k,
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalSeedream4<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Image URLs for editing. Empty vec = text-to-image mode.
|
||||
pub image_urls: Vec<String>,
|
||||
pub image_size: Option<FalSeedream4ImageSize>,
|
||||
pub num_images: FalSeedream4NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_seedream_4<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalSeedream4(PlanFalSeedream4 {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
image_size,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef<'_>>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
is_edit_mode: bool,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<FalSeedream4ImageSize>, ArtcraftRouterError> {
|
||||
use FalSeedream4ImageSize as S;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto) => {
|
||||
if is_edit_mode { Ok(Some(S::Auto)) } else { Ok(Some(S::Square)) }
|
||||
}
|
||||
Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
|
||||
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::LandscapeSixteenNine)),
|
||||
},
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::TallFourByFive)
|
||||
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
|
||||
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::PortraitSixteenNine)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalSeedream4NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalSeedream4NumImages::One),
|
||||
2 => Ok(FalSeedream4NumImages::Two),
|
||||
3 => Ok(FalSeedream4NumImages::Three),
|
||||
4 => Ok(FalSeedream4NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalSeedream4NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream4NumImages {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4TextToImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV4TextToImageNumImages as T;
|
||||
match self {
|
||||
Self::One => T::One,
|
||||
Self::Two => T::Two,
|
||||
Self::Three => T::Three,
|
||||
Self::Four => T::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4EditImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV4EditImageNumImages as E;
|
||||
match self {
|
||||
Self::One => E::One,
|
||||
Self::Two => E::Two,
|
||||
Self::Three => E::Three,
|
||||
Self::Four => E::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream4ImageSize {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4TextToImageSize {
|
||||
use EnqueueBytedanceSeedreamV4TextToImageSize as T;
|
||||
match self {
|
||||
Self::Square => T::Square,
|
||||
Self::SquareHd => T::SquareHd,
|
||||
Self::PortraitFourThree => T::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => T::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => T::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => T::LandscapeSixteenNine,
|
||||
Self::Auto => T::Auto,
|
||||
Self::Auto2k => T::Auto2k,
|
||||
Self::Auto4k => T::Auto4k,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4EditImageSize {
|
||||
use EnqueueBytedanceSeedreamV4EditImageSize as E;
|
||||
match self {
|
||||
Self::Square => E::Square,
|
||||
Self::SquareHd => E::SquareHd,
|
||||
Self::PortraitFourThree => E::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => E::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => E::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => E::LandscapeSixteenNine,
|
||||
Self::Auto => E::Auto,
|
||||
Self::Auto2k => E::Auto2k,
|
||||
Self::Auto4k => E::Auto4k,
|
||||
}
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV4p5EditImageNumImages, EnqueueBytedanceSeedreamV4p5EditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV4p5TextToImageNumImages, EnqueueBytedanceSeedreamV4p5TextToImageSize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream4p5NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
/// Note: Seedream V4.5 lacks the bare "Auto" image_size; only Auto2k/Auto4k are supported.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream4p5ImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
Auto2k,
|
||||
Auto4k,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalSeedream4p5<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub image_urls: Vec<String>,
|
||||
pub image_size: Option<FalSeedream4p5ImageSize>,
|
||||
pub num_images: FalSeedream4p5NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_seedream_4p5<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalSeedream4p5(PlanFalSeedream4p5 {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
image_size,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef<'_>>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<FalSeedream4p5ImageSize>, ArtcraftRouterError> {
|
||||
use FalSeedream4p5ImageSize as S;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
// No bare Auto for v4.5 — fall back to Auto2k.
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
|
||||
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::LandscapeSixteenNine)),
|
||||
},
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::TallFourByFive)
|
||||
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
|
||||
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::PortraitSixteenNine)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalSeedream4p5NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalSeedream4p5NumImages::One),
|
||||
2 => Ok(FalSeedream4p5NumImages::Two),
|
||||
3 => Ok(FalSeedream4p5NumImages::Three),
|
||||
4 => Ok(FalSeedream4p5NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalSeedream4p5NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream4p5NumImages {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4p5TextToImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV4p5TextToImageNumImages as T;
|
||||
match self {
|
||||
Self::One => T::One,
|
||||
Self::Two => T::Two,
|
||||
Self::Three => T::Three,
|
||||
Self::Four => T::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4p5EditImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV4p5EditImageNumImages as E;
|
||||
match self {
|
||||
Self::One => E::One,
|
||||
Self::Two => E::Two,
|
||||
Self::Three => E::Three,
|
||||
Self::Four => E::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream4p5ImageSize {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4p5TextToImageSize {
|
||||
use EnqueueBytedanceSeedreamV4p5TextToImageSize as T;
|
||||
match self {
|
||||
Self::Square => T::Square,
|
||||
Self::SquareHd => T::SquareHd,
|
||||
Self::PortraitFourThree => T::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => T::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => T::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => T::LandscapeSixteenNine,
|
||||
Self::Auto2k => T::Auto2k,
|
||||
Self::Auto4k => T::Auto4k,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4p5EditImageSize {
|
||||
use EnqueueBytedanceSeedreamV4p5EditImageSize as E;
|
||||
match self {
|
||||
Self::Square => E::Square,
|
||||
Self::SquareHd => E::SquareHd,
|
||||
Self::PortraitFourThree => E::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => E::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => E::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => E::LandscapeSixteenNine,
|
||||
Self::Auto2k => E::Auto2k,
|
||||
Self::Auto4k => E::Auto4k,
|
||||
}
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV5LiteEditImageNumImages, EnqueueBytedanceSeedreamV5LiteEditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{
|
||||
EnqueueBytedanceSeedreamV5LiteTextToImageNumImages, EnqueueBytedanceSeedreamV5LiteTextToImageSize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream5LiteNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
/// Note: Seedream V5 Lite supports Auto2k and Auto3k (no 4K, no bare Auto).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalSeedream5LiteImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
Auto2k,
|
||||
Auto3k,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalSeedream5Lite<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub image_urls: Vec<String>,
|
||||
pub image_size: Option<FalSeedream5LiteImageSize>,
|
||||
pub num_images: FalSeedream5LiteNumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_seedream_5_lite<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let image_urls = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, request.resolution, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::FalSeedream5Lite(PlanFalSeedream5Lite {
|
||||
prompt: request.prompt,
|
||||
image_urls,
|
||||
image_size,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef<'_>>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<FalSeedream5LiteImageSize>, ArtcraftRouterError> {
|
||||
use FalSeedream5LiteImageSize as S;
|
||||
|
||||
// Resolution-based auto sizes take precedence when an explicit Auto* aspect ratio
|
||||
// isn't specified but a resolution is — Seedream v5 lite has no 4K, fall back to 3K.
|
||||
match aspect_ratio {
|
||||
None => match resolution {
|
||||
Some(CommonResolution::TwoK) => Ok(Some(S::Auto2k)),
|
||||
Some(CommonResolution::ThreeK) | Some(CommonResolution::FourK) => Ok(Some(S::Auto3k)),
|
||||
_ => Ok(None),
|
||||
},
|
||||
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
|
||||
// No 4K — fall back to 3K.
|
||||
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto3k)),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
|
||||
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::LandscapeSixteenNine)),
|
||||
},
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::TallFourByFive)
|
||||
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
|
||||
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::PortraitSixteenNine)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FalSeedream5LiteNumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalSeedream5LiteNumImages::One),
|
||||
2 => Ok(FalSeedream5LiteNumImages::Two),
|
||||
3 => Ok(FalSeedream5LiteNumImages::Three),
|
||||
4 => Ok(FalSeedream5LiteNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalSeedream5LiteNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream5LiteNumImages {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV5LiteTextToImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV5LiteTextToImageNumImages as T;
|
||||
match self {
|
||||
Self::One => T::One,
|
||||
Self::Two => T::Two,
|
||||
Self::Three => T::Three,
|
||||
Self::Four => T::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV5LiteEditImageNumImages {
|
||||
use EnqueueBytedanceSeedreamV5LiteEditImageNumImages as E;
|
||||
match self {
|
||||
Self::One => E::One,
|
||||
Self::Two => E::Two,
|
||||
Self::Three => E::Three,
|
||||
Self::Four => E::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalSeedream5LiteImageSize {
|
||||
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV5LiteTextToImageSize {
|
||||
use EnqueueBytedanceSeedreamV5LiteTextToImageSize as T;
|
||||
match self {
|
||||
Self::Square => T::Square,
|
||||
Self::SquareHd => T::SquareHd,
|
||||
Self::PortraitFourThree => T::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => T::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => T::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => T::LandscapeSixteenNine,
|
||||
Self::Auto2k => T::Auto2k,
|
||||
Self::Auto3k => T::Auto3k,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV5LiteEditImageSize {
|
||||
use EnqueueBytedanceSeedreamV5LiteEditImageSize as E;
|
||||
match self {
|
||||
Self::Square => E::Square,
|
||||
Self::SquareHd => E::SquareHd,
|
||||
Self::PortraitFourThree => E::PortraitFourThree,
|
||||
Self::PortraitSixteenNine => E::PortraitSixteenNine,
|
||||
Self::LandscapeFourThree => E::LandscapeFourThree,
|
||||
Self::LandscapeSixteenNine => E::LandscapeSixteenNine,
|
||||
Self::Auto2k => E::Auto2k,
|
||||
Self::Auto3k => E::Auto3k,
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -37,8 +37,9 @@ pub enum FluxPro11NumImages {
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for FluxPro11Args<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Your request will cost $0.04 per megapixel. -- NB: What? Not going to calculate that.
|
||||
let base_cost = 6;
|
||||
// Your request will cost $0.04 per megapixel, billed by rounding up to the nearest
|
||||
// megapixel. Default image_size values are ~1MP, so 4 cents per image.
|
||||
let base_cost = 4;
|
||||
let cost = match self.num_images {
|
||||
FluxPro11NumImages::One => base_cost,
|
||||
FluxPro11NumImages::Two => base_cost * 2,
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
use actix_web::cookie::time::OffsetDateTime;
|
||||
use actix_web::cookie::{Cookie, SameSite};
|
||||
use actix_web::HttpRequest;
|
||||
use log::warn;
|
||||
|
||||
use crate::sessions::anonymous_visitor_tracking::avt_cookie_payload::AvtCookiePayload;
|
||||
use crate::sessions::anonymous_visitor_tracking::avt_cookie_payload_error::AvtCookiePayloadError;
|
||||
use crate::sessions::anonymous_visitor_tracking::avt_payload_signer::AvtPayloadSigner;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
|
||||
const VISITOR_COOKIE_NAME : &str = "visitor";
|
||||
|
||||
/// Handle "anonymous visitor tracking" cookies.
|
||||
/// This enables us to associate results with an anonymous user for a better experience,
|
||||
/// as well as do some form of return visitor tracking.
|
||||
#[derive(Clone)]
|
||||
pub struct AvtCookieManager {
|
||||
cookie_domain: String,
|
||||
payload_signer: AvtPayloadSigner,
|
||||
}
|
||||
|
||||
impl AvtCookieManager {
|
||||
|
||||
pub fn new(cookie_domain: &str, hmac_secret: &str) -> Result<Self, AvtCookiePayloadError> {
|
||||
Ok(Self {
|
||||
cookie_domain: cookie_domain.to_string(),
|
||||
payload_signer: AvtPayloadSigner::new(hmac_secret)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn make_new_cookie(&self) -> Result<Cookie, AvtCookiePayloadError> {
|
||||
let token = AnonymousVisitorTrackingToken::generate();
|
||||
self.make_new_cookie_with_apriori_token(&token)
|
||||
}
|
||||
|
||||
pub fn make_new_cookie_with_apriori_token(
|
||||
&self,
|
||||
token: &AnonymousVisitorTrackingToken,
|
||||
) -> Result<Cookie, AvtCookiePayloadError> {
|
||||
let payload = AvtCookiePayload::from_token(token.clone());
|
||||
let jwt_string = self.payload_signer.encode(&payload)?;
|
||||
|
||||
let make_secure = !self.cookie_domain.to_lowercase().contains("jungle.horse")
|
||||
&& !self.cookie_domain.to_lowercase().contains("localhost");
|
||||
|
||||
let same_site = if make_secure {
|
||||
SameSite::None // NB: Allow usage from other domains
|
||||
} else {
|
||||
SameSite::Lax // NB: You can't set "SameSite=None" on non-secure cookies
|
||||
};
|
||||
|
||||
Ok(Cookie::build(VISITOR_COOKIE_NAME, jwt_string)
|
||||
.secure(make_secure) // HTTPS-only
|
||||
.same_site(same_site)
|
||||
.permanent()
|
||||
.path("/") // NB: Otherwise it'll be set to `/v1`
|
||||
//.domain(&self.cookie_domain)
|
||||
//.http_only(true) // Not exposed to Javascript
|
||||
.finish())
|
||||
}
|
||||
|
||||
pub fn make_delete_cookie(&self) -> Cookie {
|
||||
let mut cookie = Cookie::build(VISITOR_COOKIE_NAME, "DELETED")
|
||||
.expires(OffsetDateTime::UNIX_EPOCH)
|
||||
.path("/") // NB: Otherwise it'll be set to `/v1`
|
||||
.finish();
|
||||
cookie.make_removal();
|
||||
cookie
|
||||
}
|
||||
|
||||
pub fn decode_cookie_payload(
|
||||
&self,
|
||||
visitor_cookie: &Cookie,
|
||||
) -> Result<AvtCookiePayload, AvtCookiePayloadError> {
|
||||
self.payload_signer.decode(visitor_cookie.value())
|
||||
}
|
||||
|
||||
pub fn decode_cookie_payload_from_request(
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
) -> Result<Option<AvtCookiePayload>, AvtCookiePayloadError> {
|
||||
let cookie = match request.cookie(VISITOR_COOKIE_NAME) {
|
||||
None => return Ok(None),
|
||||
Some(cookie) => cookie,
|
||||
};
|
||||
|
||||
match self.decode_cookie_payload(&cookie) {
|
||||
Err(e) => {
|
||||
warn!("Visitor cookie decode error: {:?}", e);
|
||||
Err(e)
|
||||
},
|
||||
Ok(payload) => Ok(Some(payload)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_avt_token_from_request(
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
) -> Option<AnonymousVisitorTrackingToken> {
|
||||
self.decode_cookie_payload_from_request(request)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|payload| payload.avt_token)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::test::TestRequest;
|
||||
|
||||
use crate::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
|
||||
#[test]
|
||||
fn test_create_cookie() {
|
||||
// NB: Stable encoding test. If this changes we should bump the cookie version
|
||||
// so we don't accidentally invalidate visitor tracking on existing cookies.
|
||||
let manager = AvtCookieManager::new("fakeyou.com", "secret").unwrap();
|
||||
let token = AnonymousVisitorTrackingToken::new_from_str("avt_ex_anonymous_visitor_tracking_token");
|
||||
let cookie = manager.make_new_cookie_with_apriori_token(&token).unwrap();
|
||||
|
||||
assert_eq!(cookie.value(), "eyJhbGciOiJIUzI1NiJ9.eyJhdnRfdG9rZW4iOiJhdnRfZXhfYW5vbnltb3VzX3Zpc2l0b3JfdHJhY2tpbmdfdG9rZW4iLCJjb29raWVfdmVyc2lvbiI6IjEifQ.w0KRxBxiyBfTZGxaezRa0IgD8ojmCnhRH4SS33Nq3O8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cookie_round_trip() {
|
||||
let manager = AvtCookieManager::new("fakeyou.com", "secret").unwrap();
|
||||
let token = AnonymousVisitorTrackingToken::new_from_str("avt_ex_anonymous_visitor_tracking_token");
|
||||
let cookie = manager.make_new_cookie_with_apriori_token(&token).unwrap();
|
||||
|
||||
let http_request = TestRequest::default()
|
||||
.cookie(cookie)
|
||||
.to_http_request();
|
||||
|
||||
let decoded = manager.decode_cookie_payload_from_request(&http_request)
|
||||
.expect("no error")
|
||||
.expect("must exist");
|
||||
|
||||
assert_eq!(decoded.avt_token.as_str(), "avt_ex_anonymous_visitor_tracking_token");
|
||||
assert_eq!(decoded.cookie_version, 1);
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::error::internal_error::InternalError;
|
||||
use crate::sessions::anonymous_visitor_tracking::avt_cookie_payload_error::AvtCookiePayloadError;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
|
||||
/**
|
||||
@@ -43,18 +43,18 @@ impl AvtCookiePayload {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_map(map: BTreeMap<String, String>) -> Result<Self, InternalError> {
|
||||
pub fn from_map(map: BTreeMap<String, String>) -> Result<Self, AvtCookiePayloadError> {
|
||||
let avt_token = map.get("avt_token")
|
||||
.ok_or_else(|| InternalError::VisitorCookieMissingField("avt_token"))?;
|
||||
.ok_or(AvtCookiePayloadError::MissingField("avt_token"))?;
|
||||
|
||||
let cookie_version = map
|
||||
.get("cookie_version")
|
||||
.ok_or_else(|| InternalError::VisitorCookieMissingField("cookie_version"))?;
|
||||
.ok_or(AvtCookiePayloadError::MissingField("cookie_version"))?;
|
||||
|
||||
let avt_token = AnonymousVisitorTrackingToken::new_from_str(avt_token);
|
||||
|
||||
let cookie_version = u32::from_str(cookie_version)
|
||||
.map_err(|e| InternalError::VisitorCookieError(
|
||||
.map_err(|e| AvtCookiePayloadError::PayloadDecodeError(
|
||||
format!("invalid integer for cookie_version: {:?}, version: {}", e, cookie_version)))?;
|
||||
|
||||
Ok(AvtCookiePayload {
|
||||
@@ -75,7 +75,7 @@ impl AvtCookiePayload {
|
||||
mod tests {
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
|
||||
use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_payload::AvtCookiePayload;
|
||||
use crate::sessions::anonymous_visitor_tracking::avt_cookie_payload::AvtCookiePayload;
|
||||
|
||||
#[test]
|
||||
fn test_new() {
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use jwt_signer::jwt_signer_error::JwtSignerError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AvtCookiePayloadError {
|
||||
/// AVT cookie payload is missing a required field.
|
||||
MissingField(&'static str),
|
||||
|
||||
/// Generic decode error (e.g. invalid integer for cookie_version).
|
||||
PayloadDecodeError(String),
|
||||
|
||||
/// Error encoding, decoding, or constructing the JWT signer.
|
||||
JwtSigner(JwtSignerError),
|
||||
}
|
||||
|
||||
impl AvtCookiePayloadError {
|
||||
pub fn is_server_error(&self) -> bool {
|
||||
match self {
|
||||
// JWT verify errors (eg. forged cookies) → 400 bad input.
|
||||
AvtCookiePayloadError::JwtSigner(JwtSignerError::JwtVerifyError(_)) => {
|
||||
false
|
||||
},
|
||||
// Server-side JWT signer failures (bad HMAC config, signing failure) → 500.
|
||||
AvtCookiePayloadError::JwtSigner(
|
||||
JwtSignerError::JwtInvalidKeyLength | JwtSignerError::JwtSignError(_)
|
||||
) => {
|
||||
true
|
||||
},
|
||||
// Other payload decode failures (how did these make it into the wild!?) → 500
|
||||
AvtCookiePayloadError::MissingField(_) | AvtCookiePayloadError::PayloadDecodeError(_) => {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AvtCookiePayloadError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::MissingField(field) => write!(f, "AVT cookie missing field: {}", field),
|
||||
Self::PayloadDecodeError(reason) => write!(f, "AVT cookie decode error: {}", reason),
|
||||
Self::JwtSigner(e) => write!(f, "JWT signer error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for AvtCookiePayloadError {}
|
||||
|
||||
impl From<JwtSignerError> for AvtCookiePayloadError {
|
||||
fn from(err: JwtSignerError) -> Self {
|
||||
Self::JwtSigner(err)
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
use crate::sessions::anonymous_visitor_tracking::avt_cookie_payload::AvtCookiePayload;
|
||||
use crate::sessions::anonymous_visitor_tracking::avt_cookie_payload_error::AvtCookiePayloadError;
|
||||
use jwt_signer::jwt_signer::JwtSigner;
|
||||
|
||||
/// Signs and verifies the AVT cookie payload using HMAC-signed JWTs.
|
||||
/// This module is intentionally HTTP-agnostic. The HTTP / cookie wrapper lives in `avt_cookie_manager`.
|
||||
#[derive(Clone)]
|
||||
pub struct AvtPayloadSigner {
|
||||
jwt_signer: JwtSigner,
|
||||
}
|
||||
|
||||
impl AvtPayloadSigner {
|
||||
pub fn new(hmac_secret: &str) -> Result<Self, AvtCookiePayloadError> {
|
||||
Ok(Self {
|
||||
jwt_signer: JwtSigner::new(hmac_secret)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(&self, payload: &AvtCookiePayload) -> Result<String, AvtCookiePayloadError> {
|
||||
let claims = payload.to_map();
|
||||
let jwt_string = self.jwt_signer.claims_to_jwt(&claims)?;
|
||||
Ok(jwt_string)
|
||||
}
|
||||
|
||||
pub fn decode(&self, jwt: &str) -> Result<AvtCookiePayload, AvtCookiePayloadError> {
|
||||
let claims = self.jwt_signer.jwt_to_claims(jwt)?;
|
||||
AvtCookiePayload::from_map(claims)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::sessions::anonymous_visitor_tracking::avt_cookie_payload::AvtCookiePayload;
|
||||
use crate::sessions::anonymous_visitor_tracking::avt_payload_signer::AvtPayloadSigner;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
|
||||
#[test]
|
||||
fn test_encode() {
|
||||
// NB: Stable encoding test. If this changes we should bump the cookie version.
|
||||
let signer = AvtPayloadSigner::new("fake_secret").unwrap();
|
||||
let token = AnonymousVisitorTrackingToken::new_from_str("avt_ex_anonymous_visitor_tracking_token");
|
||||
let payload = AvtCookiePayload::from_token(token);
|
||||
|
||||
let encoded = signer.encode(&payload).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
encoded,
|
||||
"eyJhbGciOiJIUzI1NiJ9.eyJhdnRfdG9rZW4iOiJhdnRfZXhfYW5vbnltb3VzX3Zpc2l0b3JfdHJhY2tpbmdfdG9rZW4iLCJjb29raWVfdmVyc2lvbiI6IjEifQ.5jgQfSXue684okKBzW3847jEhL7eLQKzelyAkgnwe_I"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_version_1() {
|
||||
// NB: Version 1 payload. The expected encoded value below was generated by `test_encode`.
|
||||
let payload =
|
||||
"eyJhbGciOiJIUzI1NiJ9.eyJhdnRfdG9rZW4iOiJhdnRfZXhfYW5vbnltb3VzX3Zpc2l0b3JfdHJhY2tpbmdfdG9rZW4iLCJjb29raWVfdmVyc2lvbiI6IjEifQ.5jgQfSXue684okKBzW3847jEhL7eLQKzelyAkgnwe_I";
|
||||
|
||||
let signer = AvtPayloadSigner::new("fake_secret").unwrap();
|
||||
let decoded = signer.decode(payload).unwrap();
|
||||
|
||||
assert_eq!(decoded.avt_token.as_str(), "avt_ex_anonymous_visitor_tracking_token");
|
||||
assert_eq!(decoded.cookie_version, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip() {
|
||||
let signer = AvtPayloadSigner::new("fake_secret").unwrap();
|
||||
let token = AnonymousVisitorTrackingToken::new_from_str("avt_ex_anonymous_visitor_tracking_token");
|
||||
let payload = AvtCookiePayload::from_token(token);
|
||||
|
||||
let encoded = signer.encode(&payload).unwrap();
|
||||
let decoded = signer.decode(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded.avt_token.as_str(), "avt_ex_anonymous_visitor_tracking_token");
|
||||
assert_eq!(decoded.cookie_version, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod avt_cookie_manager;
|
||||
pub mod avt_cookie_payload;
|
||||
pub mod avt_cookie_payload_error;
|
||||
pub mod avt_payload_signer;
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpUserSessionPayload {
|
||||
/// The database primary key for the session instance.
|
||||
pub session_token: String,
|
||||
|
||||
/// The primary key identifier of the user.
|
||||
/// Version 1 cookies do not have a user token, hence it is optional.
|
||||
pub maybe_user_token: Option<String>,
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod http_user_session_manager;
|
||||
pub mod http_user_session_payload;
|
||||
pub mod payload_signer;
|
||||
pub mod anonymous_visitor_tracking;
|
||||
pub mod user_sessions;
|
||||
|
||||
+55
-23
@@ -6,12 +6,11 @@
|
||||
use actix_web::cookie::time::OffsetDateTime;
|
||||
use actix_web::cookie::{Cookie, SameSite};
|
||||
use actix_web::HttpRequest;
|
||||
use anyhow::anyhow;
|
||||
use log::warn;
|
||||
|
||||
use crate::sessions::http_user_session_payload::HttpUserSessionPayload;
|
||||
use crate::sessions::payload_signer::HttpUserSessionPayloadSigner;
|
||||
use errors::AnyhowResult;
|
||||
use crate::sessions::user_sessions::http_user_session_payload::HttpUserSessionPayload;
|
||||
use crate::sessions::user_sessions::http_user_session_payload_signer::HttpUserSessionPayloadSigner;
|
||||
use crate::sessions::user_sessions::http_user_session_payload_error::HttpUserSessionPayloadError;
|
||||
use tokens::tokens::user_sessions::UserSessionToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
@@ -31,19 +30,41 @@ pub struct HttpUserSessionManager {
|
||||
}
|
||||
|
||||
impl HttpUserSessionManager {
|
||||
pub fn new(cookie_domain: &str, hmac_secret: &str) -> AnyhowResult<Self> {
|
||||
pub fn new(cookie_domain: &str, hmac_secret: &str) -> Result<Self, HttpUserSessionPayloadError> {
|
||||
let payload_signer = HttpUserSessionPayloadSigner::new(hmac_secret)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to construct HttpUserSessionPayloadSigner: {}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
cookie_domain: cookie_domain.to_string(),
|
||||
payload_signer: HttpUserSessionPayloadSigner::new(hmac_secret)?,
|
||||
payload_signer,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode_session_payload(&self, session_token: &UserSessionToken, user_token: &UserToken) -> AnyhowResult<String> {
|
||||
pub fn encode_session_payload(
|
||||
&self,
|
||||
session_token: &UserSessionToken,
|
||||
user_token: &UserToken,
|
||||
) -> Result<String, HttpUserSessionPayloadError> {
|
||||
self.payload_signer.encode(session_token, user_token)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to encode session payload: {}", e);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_cookie(&self, session_token: &UserSessionToken, user_token: &UserToken) -> AnyhowResult<Cookie> {
|
||||
let jwt_string = self.payload_signer.encode(session_token, user_token)?;
|
||||
pub fn create_cookie(
|
||||
&self,
|
||||
session_token: &UserSessionToken,
|
||||
user_token: &UserToken,
|
||||
) -> Result<Cookie, HttpUserSessionPayloadError> {
|
||||
let jwt_string = self.payload_signer.encode(session_token, user_token)
|
||||
.map_err(|e| {
|
||||
warn!("Failed to encode session cookie payload: {}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
let make_secure = !self.cookie_domain.to_lowercase().contains("jungle.horse")
|
||||
&& !self.cookie_domain.to_lowercase().contains("localhost");
|
||||
@@ -76,8 +97,8 @@ impl HttpUserSessionManager {
|
||||
|
||||
pub fn decode_session_payload_from_request(
|
||||
&self,
|
||||
request: &HttpRequest
|
||||
) -> AnyhowResult<Option<HttpUserSessionPayload>>
|
||||
request: &HttpRequest,
|
||||
) -> Result<Option<HttpUserSessionPayload>, HttpUserSessionPayloadError>
|
||||
{
|
||||
let signed_session_payload = self.session_payload_from_request(request)?;
|
||||
|
||||
@@ -88,8 +109,8 @@ impl HttpUserSessionManager {
|
||||
|
||||
match self.payload_signer.decode(&signed_session_payload) {
|
||||
Err(e) => {
|
||||
warn!("Session cookie decode error: {:?}", e);
|
||||
Err(anyhow!("Could not decode session cookie: {:?}", e))
|
||||
warn!("Session cookie decode error: {}", e);
|
||||
Err(e)
|
||||
},
|
||||
Ok(payload) => Ok(Some(payload)),
|
||||
}
|
||||
@@ -97,7 +118,10 @@ impl HttpUserSessionManager {
|
||||
|
||||
// NB: THIS IS ONLY FOR A QUICK HACK FOR FREAKING CORS UGH
|
||||
// THIS IS A HUGE STUPID SECURITY VULN. DAMNIT GOOGLE DAMNIT CORS.
|
||||
pub fn check_and_return_session_token_decodes(&self, request: &HttpRequest) -> AnyhowResult<Option<String>> {
|
||||
pub fn check_and_return_session_token_decodes(
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
) -> Result<Option<String>, HttpUserSessionPayloadError> {
|
||||
let signed_session_payload = self.session_payload_from_request(request)?;
|
||||
|
||||
let signed_session_payload = match signed_session_payload {
|
||||
@@ -107,8 +131,8 @@ impl HttpUserSessionManager {
|
||||
|
||||
match self.payload_signer.decode(&signed_session_payload) {
|
||||
Err(e) => {
|
||||
warn!("Session cookie decode error: {:?}", e);
|
||||
return Err(anyhow!("Could not decode session cookie: {:?}", e));
|
||||
warn!("Session cookie decode error: {}", e);
|
||||
return Err(e);
|
||||
},
|
||||
Ok(_payload) => {}, // Good! We'll just discard this.
|
||||
}
|
||||
@@ -116,15 +140,23 @@ impl HttpUserSessionManager {
|
||||
Ok(Some(signed_session_payload))
|
||||
}
|
||||
|
||||
fn session_payload_from_request(&self, request: &HttpRequest) -> AnyhowResult<Option<String>> {
|
||||
let mut signed_session_payload= request.cookie(SESSION_COOKIE_NAME)
|
||||
fn session_payload_from_request(
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
) -> Result<Option<String>, HttpUserSessionPayloadError> {
|
||||
let mut signed_session_payload = request.cookie(SESSION_COOKIE_NAME)
|
||||
.map(|cookie| cookie.value().to_string());
|
||||
|
||||
if signed_session_payload.is_none() {
|
||||
signed_session_payload = request.headers().get(SESSION_HEADER_NAME)
|
||||
.map(|header| header.to_str())
|
||||
.transpose()?
|
||||
.map(|payload| payload.to_string());
|
||||
let maybe_header = request.headers().get(SESSION_HEADER_NAME);
|
||||
if let Some(header) = maybe_header {
|
||||
let header_str = header.to_str()
|
||||
.map_err(|e| {
|
||||
warn!("Failed to read session HTTP header as str: {}", e);
|
||||
HttpUserSessionPayloadError::HttpSessionHeaderError(e.to_string())
|
||||
})?;
|
||||
signed_session_payload = Some(header_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(signed_session_payload)
|
||||
@@ -138,7 +170,7 @@ mod tests {
|
||||
use tokens::tokens::user_sessions::UserSessionToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use crate::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
|
||||
#[test]
|
||||
fn test_create_cookie_payload() {
|
||||
@@ -0,0 +1,74 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::sessions::user_sessions::http_user_session_payload_error::HttpUserSessionPayloadError;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpUserSessionPayload {
|
||||
/// The database primary key for the session instance.
|
||||
pub session_token: String,
|
||||
|
||||
/// The primary key identifier of the user.
|
||||
/// Version 1 cookies do not have a user token, hence it is optional.
|
||||
pub maybe_user_token: Option<String>,
|
||||
}
|
||||
|
||||
impl HttpUserSessionPayload {
|
||||
pub fn from_map(
|
||||
map: BTreeMap<String, String>,
|
||||
) -> Result<Self, HttpUserSessionPayloadError> {
|
||||
let session_token = map
|
||||
.get("session_token")
|
||||
.ok_or(HttpUserSessionPayloadError::MissingField("session_token"))?
|
||||
.clone();
|
||||
|
||||
let maybe_user_token = map.get("user_token").map(|t| t.to_string());
|
||||
|
||||
Ok(Self {
|
||||
session_token,
|
||||
maybe_user_token,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn map_of(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
|
||||
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_map_with_session_and_user_tokens() {
|
||||
let payload = HttpUserSessionPayload::from_map(map_of(&[
|
||||
("session_token", "ex_session_token"),
|
||||
("user_token", "ex_user_token"),
|
||||
("version", "3"),
|
||||
])).unwrap();
|
||||
|
||||
assert_eq!(payload.session_token, "ex_session_token");
|
||||
assert_eq!(payload.maybe_user_token.as_deref(), Some("ex_user_token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_map_without_user_token_is_ok() {
|
||||
let payload = HttpUserSessionPayload::from_map(map_of(&[
|
||||
("session_token", "ex_session_token"),
|
||||
])).unwrap();
|
||||
|
||||
assert_eq!(payload.session_token, "ex_session_token");
|
||||
assert!(payload.maybe_user_token.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_map_missing_session_token_errors() {
|
||||
let result = HttpUserSessionPayload::from_map(map_of(&[
|
||||
("user_token", "ex_user_token"),
|
||||
]));
|
||||
|
||||
match result.err().expect("expected error") {
|
||||
HttpUserSessionPayloadError::MissingField("session_token") => {}
|
||||
other => panic!("expected MissingField(session_token), got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use jwt_signer::jwt_signer_error::JwtSignerError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HttpUserSessionPayloadError {
|
||||
/// Error reading HTTP header
|
||||
HttpSessionHeaderError(String),
|
||||
|
||||
/// Session payload is missing a required field.
|
||||
MissingField(&'static str),
|
||||
|
||||
/// Error encoding, decoding, or constructing the JWT signer.
|
||||
JwtSigner(JwtSignerError),
|
||||
}
|
||||
|
||||
impl HttpUserSessionPayloadError {
|
||||
pub fn is_server_error(&self) -> bool {
|
||||
match self {
|
||||
// Client HTTP header errors → 400 bad input.
|
||||
HttpUserSessionPayloadError::HttpSessionHeaderError(_) => {
|
||||
false
|
||||
}
|
||||
// JWT verify errors (eg. forged cookies) → 400 bad input.
|
||||
HttpUserSessionPayloadError::JwtSigner(JwtSignerError::JwtVerifyError(_)) => {
|
||||
false
|
||||
}
|
||||
// Server-side JWT signer failures (bad HMAC config, signing failure) → 500.
|
||||
HttpUserSessionPayloadError::JwtSigner(
|
||||
JwtSignerError::JwtInvalidKeyLength | JwtSignerError::JwtSignError(_)
|
||||
) => {
|
||||
true
|
||||
},
|
||||
// Missing field in a verified JWT — how did this make it into the wild!? → 500
|
||||
HttpUserSessionPayloadError::MissingField(_) => {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HttpUserSessionPayloadError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::HttpSessionHeaderError(msg) => write!(f, "HTTP session header error: {}", msg),
|
||||
Self::MissingField(field) => write!(f, "HTTP session payload missing field: {}", field),
|
||||
Self::JwtSigner(e) => write!(f, "JWT signer error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for HttpUserSessionPayloadError {}
|
||||
|
||||
impl From<JwtSignerError> for HttpUserSessionPayloadError {
|
||||
fn from(err: JwtSignerError) -> Self {
|
||||
Self::JwtSigner(err)
|
||||
}
|
||||
}
|
||||
+17
-19
@@ -1,8 +1,8 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::sessions::http_user_session_payload::HttpUserSessionPayload;
|
||||
use crate::sessions::user_sessions::http_user_session_payload::HttpUserSessionPayload;
|
||||
use crate::sessions::user_sessions::http_user_session_payload_error::HttpUserSessionPayloadError;
|
||||
use jwt_signer::jwt_signer::JwtSigner;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::user_sessions::UserSessionToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
@@ -21,14 +21,18 @@ pub struct HttpUserSessionPayloadSigner {
|
||||
}
|
||||
|
||||
impl HttpUserSessionPayloadSigner {
|
||||
pub fn new(hmac_secret: &str) -> AnyhowResult<Self> {
|
||||
pub fn new(hmac_secret: &str) -> Result<Self, HttpUserSessionPayloadError> {
|
||||
Ok(Self {
|
||||
jwt_signer: JwtSigner::new(hmac_secret)?
|
||||
jwt_signer: JwtSigner::new(hmac_secret)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(&self, session_token: &UserSessionToken, user_token: &UserToken) -> AnyhowResult<String> {
|
||||
let mut claims = BTreeMap::new();
|
||||
pub fn encode(
|
||||
&self,
|
||||
session_token: &UserSessionToken,
|
||||
user_token: &UserToken,
|
||||
) -> Result<String, HttpUserSessionPayloadError> {
|
||||
let mut claims: BTreeMap<&str, &str> = BTreeMap::new();
|
||||
let payload_version = PAYLOAD_VERSION.to_string();
|
||||
|
||||
claims.insert("session_token", session_token.as_str());
|
||||
@@ -36,27 +40,21 @@ impl HttpUserSessionPayloadSigner {
|
||||
claims.insert("version", &payload_version);
|
||||
|
||||
let jwt_string = self.jwt_signer.claims_to_jwt(&claims)?;
|
||||
|
||||
Ok(jwt_string)
|
||||
}
|
||||
|
||||
pub fn decode(&self, session_payload_contents: &str) -> AnyhowResult<HttpUserSessionPayload> {
|
||||
let claims = self.jwt_signer.jwt_to_claims(&session_payload_contents)?;
|
||||
|
||||
let session_token = claims["session_token"].clone();
|
||||
let maybe_user_token = claims.get("user_token")
|
||||
.map(|t| t.to_string());
|
||||
|
||||
Ok(HttpUserSessionPayload {
|
||||
session_token,
|
||||
maybe_user_token,
|
||||
})
|
||||
pub fn decode(
|
||||
&self,
|
||||
session_payload_contents: &str,
|
||||
) -> Result<HttpUserSessionPayload, HttpUserSessionPayloadError> {
|
||||
let claims = self.jwt_signer.jwt_to_claims(session_payload_contents)?;
|
||||
HttpUserSessionPayload::from_map(claims)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::sessions::payload_signer::HttpUserSessionPayloadSigner;
|
||||
use crate::sessions::user_sessions::http_user_session_payload_signer::HttpUserSessionPayloadSigner;
|
||||
use tokens::tokens::user_sessions::UserSessionToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod http_user_session_manager;
|
||||
pub mod http_user_session_payload;
|
||||
pub mod http_user_session_payload_error;
|
||||
pub mod http_user_session_payload_signer;
|
||||
@@ -19,9 +19,6 @@ normal = ["workspace-build-acceleration"]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { version = "0.1", path = "../../workspace-build-acceleration" }
|
||||
|
||||
# Internal
|
||||
errors.workspace = true
|
||||
|
||||
# External
|
||||
hmac = "0.10.1" # jwt signing
|
||||
jwt = "0.13.0"
|
||||
|
||||
@@ -6,7 +6,7 @@ use jwt::SignWithKey;
|
||||
use jwt::VerifyWithKey;
|
||||
use sha2::Sha256;
|
||||
|
||||
use errors::{anyhow, AnyhowResult};
|
||||
use crate::jwt_signer_error::JwtSignerError;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JwtSigner {
|
||||
@@ -15,9 +15,9 @@ pub struct JwtSigner {
|
||||
|
||||
impl JwtSigner {
|
||||
|
||||
pub fn new(hmac_secret: &str) -> AnyhowResult<Self> {
|
||||
pub fn new(hmac_secret: &str) -> Result<Self, JwtSignerError> {
|
||||
let hmac_key = Hmac::new_varkey(hmac_secret.as_bytes())
|
||||
.map_err(|e| anyhow!("invalid hmac: {:?}", e))?;
|
||||
.map_err(|_| JwtSignerError::JwtInvalidKeyLength)?;
|
||||
|
||||
Ok(Self {
|
||||
hmac_key,
|
||||
@@ -25,33 +25,32 @@ impl JwtSigner {
|
||||
}
|
||||
|
||||
/// Turn a map of claims into a signed JWT cookie payload (string)
|
||||
pub fn claims_to_jwt(&self, claims: &impl Claimable) -> AnyhowResult<String> {
|
||||
let jwt_string = claims.sign(&self.hmac_key)?;
|
||||
Ok(jwt_string)
|
||||
pub fn claims_to_jwt(&self, claims: &impl Claimable) -> Result<String, JwtSignerError> {
|
||||
claims.sign(&self.hmac_key)
|
||||
}
|
||||
|
||||
/// Turn a JWT cookie payload (string) into a map of (key, value) claims
|
||||
pub fn jwt_to_claims(&self, jwt: &str) -> AnyhowResult<BTreeMap<String, String>> {
|
||||
let claims = jwt.verify_with_key(&self.hmac_key)?;
|
||||
Ok(claims)
|
||||
pub fn jwt_to_claims(&self, jwt: &str) -> Result<BTreeMap<String, String>, JwtSignerError> {
|
||||
jwt.verify_with_key(&self.hmac_key)
|
||||
.map_err(JwtSignerError::JwtVerifyError)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Claimable {
|
||||
fn sign(&self, hmac_key: &Hmac<Sha256>) -> AnyhowResult<String>;
|
||||
fn sign(&self, hmac_key: &Hmac<Sha256>) -> Result<String, JwtSignerError>;
|
||||
}
|
||||
|
||||
impl Claimable for BTreeMap<String, String> {
|
||||
fn sign(&self, hmac_key: &Hmac<Sha256>) -> AnyhowResult<String> {
|
||||
let claims = self.sign_with_key(hmac_key)?;
|
||||
Ok(claims)
|
||||
fn sign(&self, hmac_key: &Hmac<Sha256>) -> Result<String, JwtSignerError> {
|
||||
self.sign_with_key(hmac_key)
|
||||
.map_err(JwtSignerError::JwtSignError)
|
||||
}
|
||||
}
|
||||
|
||||
impl Claimable for BTreeMap<&str, &str> {
|
||||
fn sign(&self, hmac_key: &Hmac<Sha256>) -> AnyhowResult<String> {
|
||||
let claims = self.sign_with_key(hmac_key)?;
|
||||
Ok(claims)
|
||||
fn sign(&self, hmac_key: &Hmac<Sha256>) -> Result<String, JwtSignerError> {
|
||||
self.sign_with_key(hmac_key)
|
||||
.map_err(JwtSignerError::JwtSignError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,4 +91,4 @@ mod tests {
|
||||
|
||||
assert_eq!("eyJhbGciOiJIUzI1NiJ9.eyJiYXIiOiJubyIsImZvbyI6InllcyJ9.k4eRvIrtw4tuGDtQqSmEDngtGYAaQVRYVMCK5N3JcZA", &jwt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum JwtSignerError {
|
||||
/// Error decoding and verifying a JWT to claims
|
||||
JwtVerifyError(jwt::Error),
|
||||
|
||||
/// Error encoding and signing JWT claims
|
||||
JwtSignError(jwt::Error),
|
||||
|
||||
/// Error constructing the JWT signer.
|
||||
JwtInvalidKeyLength,
|
||||
}
|
||||
|
||||
impl Display for JwtSignerError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::JwtVerifyError(e) => write!(f, "JWT verify error: {}", e),
|
||||
Self::JwtSignError(e) => write!(f, "JWT sign error: {}", e),
|
||||
Self::JwtInvalidKeyLength => write!(f, "JWT signer construction failed: invalid key length"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for JwtSignerError {}
|
||||
@@ -18,4 +18,5 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod jwt_signer;
|
||||
pub mod jwt_signer;
|
||||
pub mod jwt_signer_error;
|
||||
+8
-14
@@ -2,13 +2,11 @@
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
|
||||
use anyhow::anyhow;
|
||||
use log::warn;
|
||||
use sqlx::{Executor, MySql};
|
||||
use sqlx::pool::PoolConnection;
|
||||
|
||||
use enums::common::visibility::Visibility;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::helpers::boolean_converters::{i8_to_bool, nullable_i8_to_bool_default_false, nullable_i8_to_optional_bool};
|
||||
@@ -99,7 +97,7 @@ impl SessionUserRecord {
|
||||
pub async fn get_user_session_by_token_pooled_connection(
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
session_token: &str,
|
||||
) -> AnyhowResult<Option<SessionUserRecord>> {
|
||||
) -> Result<Option<SessionUserRecord>, sqlx::Error> {
|
||||
get_user_session_by_token(&mut **mysql_connection, session_token).await
|
||||
}
|
||||
|
||||
@@ -107,7 +105,7 @@ pub async fn get_user_session_by_token_pooled_connection(
|
||||
pub async fn get_user_session_by_token<'e, 'c : 'e, E>(
|
||||
mysql_executor: E,
|
||||
session_token: &str,
|
||||
) -> AnyhowResult<Option<SessionUserRecord>>
|
||||
) -> Result<Option<SessionUserRecord>, sqlx::Error>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
// NB: Lookup failure is Err(RowNotFound).
|
||||
@@ -236,17 +234,13 @@ WHERE user_sessions.token = ?
|
||||
|
||||
Ok(Some(result_user_record))
|
||||
},
|
||||
Err(sqlx::Error::RowNotFound) => {
|
||||
warn!("Valid cookie; invalid session: {}", session_token);
|
||||
Ok(None)
|
||||
},
|
||||
Err(err) => {
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
warn!("Valid cookie; invalid session: {}", session_token);
|
||||
Ok(None)
|
||||
},
|
||||
_ => {
|
||||
warn!("Session query error: {:?}", err);
|
||||
Err(anyhow!("session query error: {:?}", err))
|
||||
}
|
||||
}
|
||||
warn!("Session query error: {:?}", err);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-14
@@ -2,12 +2,9 @@
|
||||
#![forbid(unused_imports)]
|
||||
#![forbid(unused_mut)]
|
||||
|
||||
use anyhow::anyhow;
|
||||
use log::warn;
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SessionRecord {
|
||||
pub session_token: String,
|
||||
@@ -17,7 +14,7 @@ pub struct SessionRecord {
|
||||
pub async fn get_user_session_by_token_light<'e, 'c : 'e, E>(
|
||||
mysql_executor: E,
|
||||
session_token: &str,
|
||||
) -> AnyhowResult<Option<SessionRecord>>
|
||||
) -> Result<Option<SessionRecord>, sqlx::Error>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
let maybe_session_record = sqlx::query_as!(
|
||||
@@ -37,17 +34,13 @@ AND deleted_at IS NULL
|
||||
|
||||
match maybe_session_record {
|
||||
Ok(session_record) => Ok(Some(session_record)),
|
||||
Err(sqlx::Error::RowNotFound) => {
|
||||
warn!("Valid cookie; invalid session: {}", session_token);
|
||||
Ok(None)
|
||||
},
|
||||
Err(err) => {
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
warn!("Valid cookie; invalid session: {}", session_token);
|
||||
Ok(None)
|
||||
},
|
||||
_ => {
|
||||
warn!("Session query error: {:?}", err);
|
||||
Err(anyhow!("session query error: {:?}", err))
|
||||
}
|
||||
}
|
||||
warn!("Session query error: {:?}", err);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,11 +362,12 @@ async fn old_dispatch(
|
||||
| InferenceCategory::BackgroundRemoval
|
||||
| InferenceCategory::ObjectGeneration
|
||||
| InferenceCategory::SplatGeneration
|
||||
| InferenceCategory::CharacterGeneration
|
||||
=> {
|
||||
// These are handled by the new dispatch
|
||||
return Err(ProcessSingleJobError::InvalidJob(
|
||||
anyhow!("these job types do not support the old-style dispatch: {:?}", job.inference_category)))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok(job_success_result)
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ use crate::endpoints::checkout_with_user_signup::user_creation_case::user_creati
|
||||
use crate::endpoints::checkout_with_user_signup::user_exists_case::user_exists_case;
|
||||
use crate::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
|
||||
use crate::utils::common_web_error::CommonWebError;
|
||||
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::stripe_artcraft::create_create_new_user_account_and_subscription_checkout::{PlanBillingCadence, SessionDetails, StripeArtcraftCreateSubscriptionCheckoutWithUserSignupRequest, StripeArtcraftCreateSubscriptionCheckoutWithUserSignupResponse, UserDetails};
|
||||
|
||||
@@ -30,7 +30,6 @@ pub mod billing;
|
||||
pub mod configs;
|
||||
pub mod docs;
|
||||
pub mod email;
|
||||
pub mod error;
|
||||
pub mod http_server;
|
||||
pub mod startup;
|
||||
pub mod state;
|
||||
|
||||
@@ -32,7 +32,6 @@ pub mod billing;
|
||||
pub mod configs;
|
||||
pub mod docs;
|
||||
pub mod email;
|
||||
pub mod error;
|
||||
pub mod http_server;
|
||||
pub mod startup;
|
||||
pub mod state;
|
||||
|
||||
@@ -56,7 +56,46 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// ...
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::FluxPro11,
|
||||
full_name: Some("FLUX 1.1 [pro]".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::SquareHd,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::Square),
|
||||
batch_size_min: Some(1),
|
||||
batch_size_max: Some(4),
|
||||
batch_size_default: Some(1),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::FluxPro11Ultra,
|
||||
full_name: Some("FLUX 1.1 [pro] ultra".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallTwoByThree,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::TallNineByTwentyOne,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::Square),
|
||||
batch_size_min: Some(1),
|
||||
batch_size_max: Some(4),
|
||||
batch_size_default: Some(1),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::NanoBanana, // NB: currently Gemini25Flash in our system
|
||||
@@ -149,6 +188,92 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::GptImage1p5,
|
||||
full_name: Some("GPT Image 1.5".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::TallTwoByThree,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::Square),
|
||||
batch_size_min: Some(1),
|
||||
batch_size_max: Some(4),
|
||||
batch_size_default: Some(1),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::Seedream4,
|
||||
full_name: Some("Seedream 4".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::Auto2k,
|
||||
CommonAspectRatio::Auto4k,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::SquareHd,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::Square),
|
||||
aspect_ratio_default_when_editing: Some(CommonAspectRatio::Auto),
|
||||
batch_size_min: Some(1),
|
||||
batch_size_max: Some(4),
|
||||
batch_size_default: Some(1),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::Seedream4p5,
|
||||
full_name: Some("Seedream 4.5".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto2k,
|
||||
CommonAspectRatio::Auto4k,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::SquareHd,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::Square),
|
||||
aspect_ratio_default_when_editing: Some(CommonAspectRatio::Auto2k),
|
||||
batch_size_min: Some(1),
|
||||
batch_size_max: Some(4),
|
||||
batch_size_default: Some(1),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenImageModelDetails {
|
||||
model: CommonImageModel::Seedream5Lite,
|
||||
full_name: Some("Seedream 5 Lite".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
image_refs_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto2k,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::SquareHd,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::Square),
|
||||
aspect_ratio_default_when_editing: Some(CommonAspectRatio::Auto2k),
|
||||
batch_size_min: Some(1),
|
||||
batch_size_max: Some(4),
|
||||
batch_size_default: Some(1),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
|
||||
@@ -166,6 +291,30 @@ fn build_omni_gen_image_model_providers() -> Vec<OmniGenImageModelProviderDetail
|
||||
model: CommonImageModel::Flux1Schnell,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenImageProviderModelDetails {
|
||||
model: CommonImageModel::FluxPro11,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenImageProviderModelDetails {
|
||||
model: CommonImageModel::FluxPro11Ultra,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenImageProviderModelDetails {
|
||||
model: CommonImageModel::GptImage1p5,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenImageProviderModelDetails {
|
||||
model: CommonImageModel::Seedream4,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenImageProviderModelDetails {
|
||||
model: CommonImageModel::Seedream4p5,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenImageProviderModelDetails {
|
||||
model: CommonImageModel::Seedream5Lite,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenImageProviderModelDetails {
|
||||
model: CommonImageModel::NanoBanana,
|
||||
overrides: None,
|
||||
|
||||
@@ -905,7 +905,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
ListPinnedMediaFilesSuccessResponse,
|
||||
ListPinnedWeightsError,
|
||||
ListPinnedWeightsSuccessResponse,
|
||||
ListSessionJobsError,
|
||||
ListSessionJobsItem,
|
||||
ListSessionJobsQueryParams,
|
||||
ListSessionJobsSuccessResponse,
|
||||
@@ -970,7 +969,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
SearchModelWeightsSortDirection,
|
||||
SearchModelWeightsSortField,
|
||||
SearchModelWeightsSuccessResponse,
|
||||
SessionInfoError,
|
||||
SessionTokenInfoError,
|
||||
SessionTokenInfoSuccessResponse,
|
||||
SetMediaFileCoverImageError,
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
|
||||
/// Any generic errors arising internally.
|
||||
/// An error class that we do not expose via the API.
|
||||
/// This should be massaged before we send it to the user.
|
||||
#[derive(Debug)]
|
||||
pub enum InternalError {
|
||||
/// Issue with HMAC decoding
|
||||
/// This is typically when using development cookies against production,
|
||||
/// or vice versa.
|
||||
VisitorCookieError(String),
|
||||
|
||||
/// Decode error with AVT cookie missing a required field.
|
||||
VisitorCookieMissingField(&'static str),
|
||||
|
||||
/// Catch instances of anyhow::Error.
|
||||
AnyhowError(anyhow::Error),
|
||||
}
|
||||
|
||||
impl Error for InternalError {}
|
||||
|
||||
impl Display for InternalError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
InternalError::VisitorCookieError(reason) => write!(f, "Visitor cookie error: {:?}", reason),
|
||||
InternalError::VisitorCookieMissingField(field) => write!(f, "Visitor cookie missing field: {}", field),
|
||||
InternalError::AnyhowError(err) => write!(f, "Anyhow error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for InternalError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
InternalError::AnyhowError(err)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod internal_error;
|
||||
+42
-4
@@ -1,10 +1,14 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::session::session_checker_error::SessionCheckerError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::RequireUserSessionError;
|
||||
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_payload_error::AvtCookiePayloadError;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_payload_error::HttpUserSessionPayloadError;
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError};
|
||||
use anyhow::anyhow;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::RequireUserSessionError;
|
||||
|
||||
/// An error type for actix-web handlers that wraps causal errors for debugging
|
||||
/// and paging while presenting safe, generic HTTP responses to users.
|
||||
@@ -194,9 +198,43 @@ impl From<RequireUserSessionError> for AdvancedCommonWebError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::http_server::common_responses::common_web_error::CommonWebError> for AdvancedCommonWebError {
|
||||
fn from(value: crate::http_server::common_responses::common_web_error::CommonWebError) -> Self {
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
impl From<HttpUserSessionPayloadError> for AdvancedCommonWebError {
|
||||
fn from(err: HttpUserSessionPayloadError) -> Self {
|
||||
if err.is_server_error() {
|
||||
Self::from_error(err)
|
||||
} else {
|
||||
Self::BadInputWithSimpleMessage("invalid session".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AvtCookiePayloadError> for AdvancedCommonWebError {
|
||||
fn from(err: AvtCookiePayloadError) -> Self {
|
||||
if err.is_server_error() {
|
||||
Self::from_error(err)
|
||||
} else {
|
||||
Self::BadInputWithSimpleMessage("invalid AVT cookie".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SessionCheckerError> for AdvancedCommonWebError {
|
||||
fn from(value: SessionCheckerError) -> Self {
|
||||
match value {
|
||||
// Bad / forged session cookie → 401 (don't page on this)
|
||||
// NOTE: If there's an elevated rate of across-the-board 401s,
|
||||
// then we probably misconfigured the HMAC secret
|
||||
SessionCheckerError::SessionPayload(_) => Self::NotAuthorized,
|
||||
// Underlying DB / cache errors → 500 with paging
|
||||
SessionCheckerError::Sqlx(err) => Self::from_error(err),
|
||||
// Likely Redis caching middleware
|
||||
SessionCheckerError::OtherError(err) => Self::from_anyhow_error(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CommonWebError> for AdvancedCommonWebError {
|
||||
fn from(value: CommonWebError) -> Self {
|
||||
match value {
|
||||
CommonWebError::BadInputWithSimpleMessage(msg) => Self::BadInputWithSimpleMessage(msg),
|
||||
CommonWebError::NotAuthorized => Self::NotAuthorized,
|
||||
|
||||
+22
@@ -4,6 +4,7 @@ use log::{error, warn};
|
||||
use mysql_queries::errors::mysql_error::{MysqlCrateErrorSubtype, MysqlError};
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use crate::http_server::session::session_checker_error::SessionCheckerError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::RequireUserSessionError;
|
||||
|
||||
#[derive(Debug, utoipa::ToSchema)]
|
||||
@@ -109,6 +110,27 @@ impl From<RequireUserSessionError> for CommonWebError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SessionCheckerError> for CommonWebError {
|
||||
fn from(value: SessionCheckerError) -> Self {
|
||||
match value {
|
||||
// Bad / forged session cookie → 401
|
||||
// NOTE: If there's an elevated rate of across-the-board 401s,
|
||||
// then we probably misconfigured the HMAC secret
|
||||
SessionCheckerError::SessionPayload(_) => Self::NotAuthorized,
|
||||
// Underlying DB / cache errors → 500
|
||||
SessionCheckerError::Sqlx(err) => {
|
||||
error!("SessionChecker sqlx error: {:?}", err);
|
||||
Self::ServerError
|
||||
},
|
||||
// Likely Redis caching middleware
|
||||
SessionCheckerError::OtherError(err) => {
|
||||
error!("SessionChecker other error: {:?}", err);
|
||||
Self::ServerError
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
use actix_web::cookie::time::OffsetDateTime;
|
||||
use actix_web::cookie::{Cookie, SameSite};
|
||||
use actix_web::HttpRequest;
|
||||
use anyhow::anyhow;
|
||||
use log::warn;
|
||||
|
||||
use crate::error::internal_error::InternalError;
|
||||
use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_payload::AvtCookiePayload;
|
||||
use jwt_signer::jwt_signer::JwtSigner;
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
|
||||
const VISITOR_COOKIE_NAME : &str = "visitor";
|
||||
|
||||
/// Handle "anonymous visitor tracking" cookies.
|
||||
/// This enables us to associate results with an anonymous user for a better experience,
|
||||
/// as well as do some form of return visitor tracking.
|
||||
#[derive(Clone)]
|
||||
pub struct AvtCookieManager {
|
||||
cookie_domain: String,
|
||||
jwt_signer: JwtSigner,
|
||||
}
|
||||
|
||||
impl AvtCookieManager {
|
||||
|
||||
pub fn new(cookie_domain: &str, hmac_secret: &str) -> AnyhowResult<Self> {
|
||||
Ok(Self {
|
||||
cookie_domain: cookie_domain.to_string(),
|
||||
jwt_signer: JwtSigner::new(hmac_secret)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn make_new_cookie(&self) -> AnyhowResult<Cookie> {
|
||||
let token = AnonymousVisitorTrackingToken::generate();
|
||||
self.make_new_cookie_with_apriori_token(&token)
|
||||
}
|
||||
|
||||
pub fn make_new_cookie_with_apriori_token(&self, token: &AnonymousVisitorTrackingToken) -> AnyhowResult<Cookie> {
|
||||
let payload = AvtCookiePayload::from_token(token.clone());
|
||||
let claims = payload.to_map();
|
||||
let jwt_string = self.jwt_signer.claims_to_jwt(&claims)?;
|
||||
|
||||
let make_secure = !self.cookie_domain.to_lowercase().contains("jungle.horse")
|
||||
&& !self.cookie_domain.to_lowercase().contains("localhost");
|
||||
|
||||
let same_site = if make_secure {
|
||||
SameSite::None // NB: Allow usage from other domains
|
||||
} else {
|
||||
SameSite::Lax // NB: You can't set "SameSite=None" on non-secure cookies
|
||||
};
|
||||
|
||||
Ok(Cookie::build(VISITOR_COOKIE_NAME, jwt_string)
|
||||
.secure(make_secure) // HTTPS-only
|
||||
.same_site(same_site)
|
||||
.permanent()
|
||||
.path("/") // NB: Otherwise it'll be set to `/v1`
|
||||
//.domain(&self.cookie_domain)
|
||||
//.http_only(true) // Not exposed to Javascript
|
||||
.finish())
|
||||
}
|
||||
|
||||
pub fn make_delete_cookie(&self) -> Cookie {
|
||||
let mut cookie = Cookie::build(VISITOR_COOKIE_NAME, "DELETED")
|
||||
.expires(OffsetDateTime::UNIX_EPOCH)
|
||||
.path("/") // NB: Otherwise it'll be set to `/v1`
|
||||
.finish();
|
||||
cookie.make_removal();
|
||||
cookie
|
||||
}
|
||||
|
||||
pub fn decode_cookie_payload(&self, visitor_cookie: &Cookie) -> AnyhowResult<AvtCookiePayload> {
|
||||
let cookie_contents = visitor_cookie.value().to_string();
|
||||
let claims = self.jwt_signer.jwt_to_claims(&cookie_contents)?;
|
||||
let payload = AvtCookiePayload::from_map(claims)?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
pub fn decode_cookie_payload_from_request(&self, request: &HttpRequest) -> Result<Option<AvtCookiePayload>, InternalError> {
|
||||
let cookie = match request.cookie(VISITOR_COOKIE_NAME) {
|
||||
None => return Ok(None),
|
||||
Some(cookie) => cookie,
|
||||
};
|
||||
|
||||
match self.decode_cookie_payload(&cookie) {
|
||||
Err(e) => {
|
||||
warn!("Visitor cookie decode error: {:?}", e);
|
||||
Err(InternalError::VisitorCookieError(e.to_string()))
|
||||
},
|
||||
Ok(payload) => Ok(Some(payload)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_avt_token_from_request(&self, request: &HttpRequest) -> Option<AnonymousVisitorTrackingToken> {
|
||||
self.decode_cookie_payload_from_request(request)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|payload| payload.avt_token)
|
||||
}
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
pub mod avt_cookie_manager;
|
||||
pub mod avt_cookie_payload;
|
||||
@@ -1 +0,0 @@
|
||||
pub mod anonymous_visitor_tracking;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use crate::http_server::endpoints::app_state::components::get_permissions::{get_permissions, AppStatePermissions};
|
||||
use crate::http_server::endpoints::app_state::components::get_premium_info::{get_premium_info, AppStatePremiumInfo};
|
||||
use crate::http_server::endpoints::app_state::components::get_server_info::{get_server_info, AppStateServerInfo};
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ pub async fn create_character_handler(
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
AdvancedCommonWebError::from(e)
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
AdvancedCommonWebError::from(e)
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
|
||||
+18
-80
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress;
|
||||
@@ -10,12 +10,9 @@ use crate::http_server::endpoints::inference_job::utils::extractors::extract_liv
|
||||
use crate::http_server::endpoints::inference_job::utils::extractors::extract_polymorphic_inference_args::extract_polymorphic_inference_args;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::filter_model_name::maybe_filter_model_name;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use actix_web_lab::extract::Query;
|
||||
use artcraft_api_defs::jobs::list_session_jobs::{ListSessionJobsItem, ListSessionJobsQueryParams, ListSessionJobsSuccessResponse, ListSessionRequestDetailsResponse, ListSessionResultDetailsResponse, ListSessionStatusDetailsResponse};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
@@ -34,7 +31,7 @@ use redis_common::redis_keys::RedisKeys;
|
||||
use server_environment::ServerEnvironment;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use utoipa::IntoParams;
|
||||
|
||||
/// For certain jobs or job classes (eg. non-premium), we kill the jobs if the user hasn't
|
||||
/// maintained a keepalive. This prevents wasted work when users who are unlikely to return
|
||||
@@ -42,40 +39,6 @@ use utoipa::{IntoParams, ToSchema};
|
||||
/// typically do not require keepalive.
|
||||
const JOB_KEEPALIVE_TTL_SECONDS : u64 = 60 * 3;
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum ListSessionJobsError {
|
||||
ServerError,
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
}
|
||||
|
||||
impl ResponseError for ListSessionJobsError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ListSessionJobsError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ListSessionJobsError::NotFound => StatusCode::NOT_FOUND,
|
||||
ListSessionJobsError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
Self::ServerError => "server error".to_string(),
|
||||
Self::NotFound => "not found".to_string(),
|
||||
Self::NotAuthorized => "not authorized".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for ListSessionJobsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// List job statuses for jobs that are associated with the user's session.
|
||||
///
|
||||
/// The user must be logged in. This endpoint will only show the last 36 hours
|
||||
@@ -89,7 +52,8 @@ impl fmt::Display for ListSessionJobsError {
|
||||
path = "/v1/jobs/session",
|
||||
responses(
|
||||
(status = 200, body = ListSessionJobsSuccessResponse),
|
||||
(status = 500, body = ListSessionJobsError),
|
||||
(status = 401, description = "Not authorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
params(
|
||||
ListSessionJobsQueryParams
|
||||
@@ -99,29 +63,21 @@ pub async fn list_session_jobs_handler(
|
||||
http_request: HttpRequest,
|
||||
query: Query<ListSessionJobsQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<ListSessionJobsSuccessResponse>, ListSessionJobsError> {
|
||||
) -> Result<Json<ListSessionJobsSuccessResponse>, AdvancedCommonWebError> {
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("MySql pool error: {:?}", err);
|
||||
ListSessionJobsError::ServerError
|
||||
})?;
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
|
||||
let maybe_avt_token = server_state.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
// ==================== USER SESSION ==================== //
|
||||
|
||||
// NB: SessionCheckerError → AdvancedCommonWebError maps payload errors (bad cookie) to 401
|
||||
// and DB / cache errors to 500.
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
ListSessionJobsError::ServerError
|
||||
})?;
|
||||
.await?;
|
||||
|
||||
let include_states = query.include_states
|
||||
.as_deref()
|
||||
@@ -135,20 +91,14 @@ pub async fn list_session_jobs_handler(
|
||||
.filter_map(|status| JobStatusPlus::from_str(status).ok())
|
||||
.collect::<HashSet<_>>());
|
||||
|
||||
let user;
|
||||
|
||||
match (maybe_user_session.as_ref(), maybe_avt_token.as_ref()) {
|
||||
(Some(session), _) => {
|
||||
user = SessionUser::User(&session.user_token_typed);
|
||||
}
|
||||
(None, Some(avt_token)) => {
|
||||
user = SessionUser::Anonymous(avt_token);
|
||||
}
|
||||
let user = match (maybe_user_session.as_ref(), maybe_avt_token.as_ref()) {
|
||||
(Some(session), _) => SessionUser::User(&session.user_token_typed),
|
||||
(None, Some(avt_token)) => SessionUser::Anonymous(avt_token),
|
||||
(None, None) => {
|
||||
// TODO(bt,2025-04-15): We should install an AVT cookie.
|
||||
return Err(ListSessionJobsError::NotAuthorized);
|
||||
return Err(AdvancedCommonWebError::NotAuthorized);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let args = ListSessionJobsForUserArgs {
|
||||
user,
|
||||
@@ -157,23 +107,11 @@ pub async fn list_session_jobs_handler(
|
||||
};
|
||||
|
||||
// NB: Since this is publicly exposed, we don't query sensitive data.
|
||||
let maybe_records = list_session_jobs_from_connection(
|
||||
args, &mut mysql_connection).await;
|
||||
|
||||
let records = match maybe_records {
|
||||
Ok(records) => records,
|
||||
Err(err) => {
|
||||
error!("tts job query error: {:?}", err);
|
||||
return Err(ListSessionJobsError::ServerError);
|
||||
}
|
||||
};
|
||||
let records = list_session_jobs_from_connection(args, &mut mysql_connection).await?;
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
error!("redis error: {:?}", e);
|
||||
ListSessionJobsError::ServerError
|
||||
})?;
|
||||
.map_err(AdvancedCommonWebError::from_error)?;
|
||||
|
||||
// TODO(bt,2024-04-22): Look up the extra redis statuses per item.
|
||||
|
||||
@@ -204,7 +142,7 @@ fn records_to_response(
|
||||
records: Vec<GenericInferenceJobStatus>,
|
||||
server_environment: ServerEnvironment,
|
||||
media_domain: MediaDomain,
|
||||
) -> Result<Json<ListSessionJobsSuccessResponse>, ListSessionJobsError> {
|
||||
) -> Result<Json<ListSessionJobsSuccessResponse>, AdvancedCommonWebError> {
|
||||
let mut records = records.into_iter()
|
||||
.map(|record| {
|
||||
db_record_to_response_payload(record, None, server_environment, media_domain)
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ pub async fn omni_gen_image_generate_handler(
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
AdvancedCommonWebError::from(e)
|
||||
})?;
|
||||
|
||||
let user_token = match maybe_user_session.as_ref() {
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ pub async fn omni_gen_video_generate_handler(
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
AdvancedCommonWebError::from_anyhow_error(e)
|
||||
AdvancedCommonWebError::from(e)
|
||||
})?;
|
||||
|
||||
let user_token = match maybe_user_session.as_ref() {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ use crate::http_server::validations::validate_passwords::validate_passwords;
|
||||
use crate::http_server::validations::validate_username::validate_username;
|
||||
use crate::util::enroll_in_studio::enroll_in_studio;
|
||||
use actix_artcraft::requests::get_request_signup_source_enum::get_request_signup_source_enum;
|
||||
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ use crate::http_server::endpoints::users::google_sso::check_claims::check_claims
|
||||
use crate::http_server::endpoints::users::google_sso::handle_existing_sso_account::{handle_existing_sso_account, ExistingAccountArgs};
|
||||
use crate::http_server::endpoints::users::google_sso::handle_new_sso_account::{handle_new_sso_account, NewSsoArgs};
|
||||
use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
|
||||
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Data, Json};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse, ResponseError};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
|
||||
+62
-112
@@ -3,60 +3,26 @@
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
use sqlx::MySqlPool;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use artcraft_api_defs::users::session_info::{FakeYouPlan, SessionInfoSuccessResponse, SessionOnboardingState, SessionUserInfo, StorytellerStreamPlan};
|
||||
use enums::by_table::users::user_feature_flag::UserFeatureFlag;
|
||||
use http_server_common::response::response_error_helpers::to_simple_json_error;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::user_details_lite_builder::UserDetailsLightBuilder;
|
||||
use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags;
|
||||
use crate::http_server::session::session_checker::SessionChecker;
|
||||
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum SessionInfoError {
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for SessionInfoError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
SessionInfoError::ServerError=> StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
SessionInfoError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for SessionInfoError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Users",
|
||||
path = "/v1/session",
|
||||
responses(
|
||||
(status = 200, description = "Get profile", body = SessionInfoSuccessResponse),
|
||||
(status = 500, description = "Server error", body = SessionInfoError),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn session_info_handler(
|
||||
@@ -64,100 +30,85 @@ pub async fn session_info_handler(
|
||||
mysql_pool: web::Data<MySqlPool>,
|
||||
session_checker: web::Data<SessionChecker>,
|
||||
avt_manager: web::Data<AvtCookieManager>,
|
||||
) -> Result<HttpResponse, SessionInfoError>
|
||||
) -> Result<HttpResponse, AdvancedCommonWebError>
|
||||
{
|
||||
let mut mysql_connection = mysql_pool.acquire()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Could not acquire DB pool: {:?}", e);
|
||||
SessionInfoError::ServerError
|
||||
})?;
|
||||
let mut mysql_connection = mysql_pool.acquire().await?;
|
||||
|
||||
let maybe_user_session = session_checker
|
||||
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
SessionInfoError::ServerError
|
||||
.map_err(|err| {
|
||||
warn!("Error checking session: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
let mut logged_in = false;
|
||||
let mut user_info = None;
|
||||
|
||||
match maybe_user_session {
|
||||
None => {}
|
||||
Some(session_data) => {
|
||||
let feature_flags =
|
||||
UserSessionFeatureFlags::new(session_data.maybe_feature_flags.as_deref());
|
||||
if let Some(session_data) = maybe_user_session {
|
||||
let feature_flags =
|
||||
UserSessionFeatureFlags::new(session_data.maybe_feature_flags.as_deref());
|
||||
|
||||
if !session_data.is_banned {
|
||||
// NB: Banned users can't be logged in
|
||||
logged_in = true;
|
||||
user_info = Some(SessionUserInfo {
|
||||
core_info: UserDetailsLightBuilder::from_db_fields(
|
||||
&session_data.user_token,
|
||||
&session_data.username,
|
||||
&session_data.display_name,
|
||||
&session_data.email_gravatar_hash,
|
||||
),
|
||||
onboarding: SessionOnboardingState {
|
||||
email_not_set: session_data.email_is_synthetic,
|
||||
email_not_confirmed: !(session_data.email_confirmed || session_data.email_confirmed_by_google),
|
||||
password_not_set: session_data.is_without_password,
|
||||
username_not_customized: session_data.username_is_not_customized,
|
||||
},
|
||||
user_token: session_data.user_token,
|
||||
username: session_data.username.to_string(),
|
||||
display_name: session_data.display_name.to_string(),
|
||||
email_gravatar_hash: session_data.email_gravatar_hash.to_string(),
|
||||
if !session_data.is_banned {
|
||||
// NB: Banned users can't be logged in
|
||||
logged_in = true;
|
||||
user_info = Some(SessionUserInfo {
|
||||
core_info: UserDetailsLightBuilder::from_db_fields(
|
||||
&session_data.user_token,
|
||||
&session_data.username,
|
||||
&session_data.display_name,
|
||||
&session_data.email_gravatar_hash,
|
||||
),
|
||||
onboarding: SessionOnboardingState {
|
||||
email_not_set: session_data.email_is_synthetic,
|
||||
email_not_confirmed: !(session_data.email_confirmed || session_data.email_confirmed_by_google),
|
||||
password_not_set: session_data.is_without_password,
|
||||
username_not_customized: session_data.username_is_not_customized,
|
||||
},
|
||||
user_token: session_data.user_token,
|
||||
username: session_data.username.to_string(),
|
||||
display_name: session_data.display_name.to_string(),
|
||||
email_gravatar_hash: session_data.email_gravatar_hash.to_string(),
|
||||
|
||||
// Rollout / feature flags:
|
||||
can_access_studio: feature_flags.has_flag(UserFeatureFlag::Studio),
|
||||
maybe_feature_flags: feature_flags.clone_flags(),
|
||||
// Rollout / feature flags:
|
||||
can_access_studio: feature_flags.has_flag(UserFeatureFlag::Studio),
|
||||
maybe_feature_flags: feature_flags.clone_flags(),
|
||||
|
||||
// Premium plans:
|
||||
fakeyou_plan: FakeYouPlan::Free,
|
||||
storyteller_stream_plan: StorytellerStreamPlan::Free,
|
||||
// Premium plans:
|
||||
fakeyou_plan: FakeYouPlan::Free,
|
||||
storyteller_stream_plan: StorytellerStreamPlan::Free,
|
||||
|
||||
// Usage permissions:
|
||||
can_use_tts: session_data.can_use_tts,
|
||||
can_use_w2l: session_data.can_use_w2l,
|
||||
can_delete_own_tts_results: session_data.can_delete_own_tts_results,
|
||||
can_delete_own_w2l_results: session_data.can_delete_own_w2l_results,
|
||||
can_delete_own_account: session_data.can_delete_own_account,
|
||||
// Usage permissions:
|
||||
can_use_tts: session_data.can_use_tts,
|
||||
can_use_w2l: session_data.can_use_w2l,
|
||||
can_delete_own_tts_results: session_data.can_delete_own_tts_results,
|
||||
can_delete_own_w2l_results: session_data.can_delete_own_w2l_results,
|
||||
can_delete_own_account: session_data.can_delete_own_account,
|
||||
|
||||
// Contribution permissions:
|
||||
can_upload_tts_models: session_data.can_upload_tts_models,
|
||||
can_upload_w2l_templates: session_data.can_upload_w2l_templates,
|
||||
can_delete_own_tts_models: session_data.can_delete_own_tts_models,
|
||||
can_delete_own_w2l_templates: session_data.can_delete_own_w2l_templates,
|
||||
// Contribution permissions:
|
||||
can_upload_tts_models: session_data.can_upload_tts_models,
|
||||
can_upload_w2l_templates: session_data.can_upload_w2l_templates,
|
||||
can_delete_own_tts_models: session_data.can_delete_own_tts_models,
|
||||
can_delete_own_w2l_templates: session_data.can_delete_own_w2l_templates,
|
||||
|
||||
// Moderation permissions:
|
||||
can_approve_w2l_templates: session_data.can_approve_w2l_templates,
|
||||
can_edit_other_users_profiles: session_data.can_edit_other_users_profiles,
|
||||
can_edit_other_users_tts_models: session_data.can_edit_other_users_tts_models,
|
||||
can_edit_other_users_w2l_templates: session_data.can_edit_other_users_w2l_templates,
|
||||
can_delete_other_users_tts_models: session_data.can_delete_other_users_tts_models,
|
||||
can_delete_other_users_tts_results: session_data.can_delete_other_users_tts_results,
|
||||
can_delete_other_users_w2l_templates: session_data.can_delete_other_users_w2l_templates,
|
||||
can_delete_other_users_w2l_results: session_data.can_delete_other_users_w2l_results,
|
||||
can_ban_users: session_data.can_ban_users,
|
||||
can_delete_users: session_data.can_delete_users,
|
||||
});
|
||||
}
|
||||
// Moderation permissions:
|
||||
can_approve_w2l_templates: session_data.can_approve_w2l_templates,
|
||||
can_edit_other_users_profiles: session_data.can_edit_other_users_profiles,
|
||||
can_edit_other_users_tts_models: session_data.can_edit_other_users_tts_models,
|
||||
can_edit_other_users_w2l_templates: session_data.can_edit_other_users_w2l_templates,
|
||||
can_delete_other_users_tts_models: session_data.can_delete_other_users_tts_models,
|
||||
can_delete_other_users_tts_results: session_data.can_delete_other_users_tts_results,
|
||||
can_delete_other_users_w2l_templates: session_data.can_delete_other_users_w2l_templates,
|
||||
can_delete_other_users_w2l_results: session_data.can_delete_other_users_w2l_results,
|
||||
can_ban_users: session_data.can_ban_users,
|
||||
can_delete_users: session_data.can_delete_users,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let maybe_avt_cookie = match avt_manager.decode_cookie_payload_from_request(&http_request) {
|
||||
Ok(Some(_avt_cookie)) => None,
|
||||
_ => {
|
||||
let cookie = avt_manager.make_new_cookie()
|
||||
.map_err(|e| {
|
||||
warn!("avt cookie creation error: {:?}", e);
|
||||
SessionInfoError::ServerError
|
||||
})?;
|
||||
Some(cookie)
|
||||
}
|
||||
_ => Some(avt_manager.make_new_cookie()?),
|
||||
};
|
||||
|
||||
let response = SessionInfoSuccessResponse {
|
||||
@@ -166,8 +117,7 @@ pub async fn session_info_handler(
|
||||
user: user_info,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|_e| SessionInfoError::ServerError)?;
|
||||
let body = serde_json::to_string(&response)?;
|
||||
|
||||
let mut response_builder = HttpResponse::Ok();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod common_requests;
|
||||
pub mod common_responses;
|
||||
pub mod endpoint_helpers;
|
||||
pub mod cookies;
|
||||
pub mod deprecated_endpoints;
|
||||
pub mod endpoints;
|
||||
pub mod middleware;
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod lookup;
|
||||
pub mod session_checker;
|
||||
pub mod session_checker;
|
||||
pub mod session_checker_error;
|
||||
@@ -3,9 +3,8 @@
|
||||
#![forbid(unused_mut)]
|
||||
#![forbid(unused_variables)]
|
||||
|
||||
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_web::HttpRequest;
|
||||
use errors::AnyhowResult;
|
||||
use log::warn;
|
||||
use mysql_queries::queries::users::user_sessions::get_user_session_by_token::{get_user_session_by_token, get_user_session_by_token_pooled_connection, SessionUserRecord};
|
||||
use mysql_queries::queries::users::user_sessions::get_user_session_by_token_light::{get_user_session_by_token_light, SessionRecord};
|
||||
@@ -17,6 +16,7 @@ use sqlx::{Executor, MySql, MySqlPool};
|
||||
|
||||
use crate::http_server::session::lookup::user_session_extended::{UserSessionExtended, UserSessionPreferences, UserSessionPremiumPlanInfo, UserSessionRoleAndPermissions, UserSessionSubscriptionPlan, UserSessionUserDetails};
|
||||
use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags;
|
||||
use crate::http_server::session::session_checker_error::SessionCheckerError;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionChecker {
|
||||
@@ -40,7 +40,7 @@ impl SessionChecker {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_session_token(&self, request: &HttpRequest) -> AnyhowResult<Option<String>> {
|
||||
pub fn get_session_token(&self, request: &HttpRequest) -> Result<Option<String>, SessionCheckerError> {
|
||||
Ok(self.cookie_manager.decode_session_payload_from_request(request)?
|
||||
.map(|payload| payload.session_token))
|
||||
}
|
||||
@@ -56,7 +56,7 @@ impl SessionChecker {
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
pool: &MySqlPool
|
||||
) -> AnyhowResult<Option<SessionRecord>>
|
||||
) -> Result<Option<SessionRecord>, SessionCheckerError>
|
||||
{
|
||||
self.do_session_light_lookup_and_cookie_decode(request, pool).await
|
||||
}
|
||||
@@ -66,7 +66,7 @@ impl SessionChecker {
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
) -> AnyhowResult<Option<SessionRecord>>
|
||||
) -> Result<Option<SessionRecord>, SessionCheckerError>
|
||||
{
|
||||
self.do_session_light_lookup_and_cookie_decode(request, &mut **mysql_connection).await
|
||||
}
|
||||
@@ -76,7 +76,7 @@ impl SessionChecker {
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
mysql_executor: E,
|
||||
) -> AnyhowResult<Option<SessionRecord>>
|
||||
) -> Result<Option<SessionRecord>, SessionCheckerError>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
let maybe_session_token = self.cookie_manager.decode_session_payload_from_request(request)?
|
||||
@@ -95,18 +95,23 @@ impl SessionChecker {
|
||||
&self,
|
||||
mysql_executor: E,
|
||||
session_token: &str,
|
||||
) -> AnyhowResult<Option<SessionRecord>>
|
||||
) -> Result<Option<SessionRecord>, SessionCheckerError>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
match self.maybe_get_redis_cache_connection() {
|
||||
None => {
|
||||
get_user_session_by_token_light(mysql_executor, session_token).await
|
||||
Ok(get_user_session_by_token_light(mysql_executor, session_token).await?)
|
||||
}
|
||||
Some(mut redis_ttl_cache) => {
|
||||
let cache_key = RedisCacheKeys::session_record_light(session_token);
|
||||
redis_ttl_cache.lazy_load_if_not_cached(&cache_key, move || {
|
||||
// NB: Redis cache requires AnyhowResult; convert sqlx::Error to anyhow inside the closure,
|
||||
// then map any error from the cache layer back to SessionCheckerError::OtherError.
|
||||
let result = redis_ttl_cache.lazy_load_if_not_cached(&cache_key, move || async move {
|
||||
get_user_session_by_token_light(mysql_executor, session_token)
|
||||
}).await
|
||||
.await
|
||||
.map_err(anyhow::Error::from)
|
||||
}).await;
|
||||
result.map_err(SessionCheckerError::OtherError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +124,7 @@ impl SessionChecker {
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
pool: &MySqlPool,
|
||||
) -> AnyhowResult<Option<SessionUserRecord>>
|
||||
) -> Result<Option<SessionUserRecord>, SessionCheckerError>
|
||||
{
|
||||
self.do_user_session_lookup_and_cookie_decode(request, pool).await
|
||||
}
|
||||
@@ -129,7 +134,7 @@ impl SessionChecker {
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
) -> AnyhowResult<Option<SessionUserRecord>>
|
||||
) -> Result<Option<SessionUserRecord>, SessionCheckerError>
|
||||
{
|
||||
self.do_user_session_lookup_and_cookie_decode(request, &mut **mysql_connection).await
|
||||
}
|
||||
@@ -139,7 +144,7 @@ impl SessionChecker {
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
mysql_executor: E,
|
||||
) -> AnyhowResult<Option<SessionUserRecord>>
|
||||
) -> Result<Option<SessionUserRecord>, SessionCheckerError>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
let session_token = match self.get_session_token(request)? {
|
||||
@@ -155,18 +160,21 @@ impl SessionChecker {
|
||||
&self,
|
||||
mysql_executor: E,
|
||||
session_token: &str,
|
||||
) -> AnyhowResult<Option<SessionUserRecord>>
|
||||
) -> Result<Option<SessionUserRecord>, SessionCheckerError>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
match self.maybe_get_redis_cache_connection() {
|
||||
None => {
|
||||
get_user_session_by_token(mysql_executor, session_token).await
|
||||
Ok(get_user_session_by_token(mysql_executor, session_token).await?)
|
||||
}
|
||||
Some(mut redis_ttl_cache) => {
|
||||
let cache_key = RedisCacheKeys::session_record_user(session_token);
|
||||
redis_ttl_cache.lazy_load_if_not_cached(&cache_key, move || {
|
||||
let result = redis_ttl_cache.lazy_load_if_not_cached(&cache_key, move || async move {
|
||||
get_user_session_by_token(mysql_executor, session_token)
|
||||
}).await
|
||||
.await
|
||||
.map_err(anyhow::Error::from)
|
||||
}).await;
|
||||
result.map_err(SessionCheckerError::OtherError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,7 +187,7 @@ impl SessionChecker {
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
pool: &MySqlPool,
|
||||
) -> AnyhowResult<Option<UserSessionExtended>>
|
||||
) -> Result<Option<UserSessionExtended>, SessionCheckerError>
|
||||
{
|
||||
let mut connection = pool.acquire().await?;
|
||||
self.maybe_get_user_session_extended_from_connection(request, &mut connection).await
|
||||
@@ -189,7 +197,7 @@ impl SessionChecker {
|
||||
&self,
|
||||
request: &HttpRequest,
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
) -> AnyhowResult<Option<UserSessionExtended>>
|
||||
) -> Result<Option<UserSessionExtended>, SessionCheckerError>
|
||||
{
|
||||
let session_payload= match self.cookie_manager.decode_session_payload_from_request(request)? {
|
||||
None => return Ok(None),
|
||||
@@ -209,7 +217,8 @@ impl SessionChecker {
|
||||
list_active_user_subscriptions(
|
||||
mysql_connection,
|
||||
user_session.user_token.as_str()
|
||||
).await?;
|
||||
).await
|
||||
.map_err(SessionCheckerError::OtherError)?;
|
||||
|
||||
Ok(Some(UserSessionExtended {
|
||||
user_token: user_session.user_token.as_str().to_string(),
|
||||
@@ -283,4 +292,3 @@ impl SessionChecker {
|
||||
result.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_payload_error::HttpUserSessionPayloadError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SessionCheckerError {
|
||||
/// Error decoding or verifying the session.
|
||||
/// Rate of failures can point to the cause:
|
||||
/// - One-off errors: probably session forgeries
|
||||
/// - Lots of errors: maybe the wrong hmac secret
|
||||
SessionPayload(HttpUserSessionPayloadError),
|
||||
|
||||
/// Error looking up the session from the database.
|
||||
Sqlx(sqlx::Error),
|
||||
|
||||
/// Some other kind of error - perhaps with the Redis middleware.
|
||||
OtherError(anyhow::Error),
|
||||
}
|
||||
|
||||
impl Display for SessionCheckerError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::SessionPayload(e) => write!(f, "session payload error: {}", e),
|
||||
Self::Sqlx(e) => write!(f, "sqlx error: {}", e),
|
||||
Self::OtherError(e) => write!(f, "other error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for SessionCheckerError {}
|
||||
|
||||
impl From<HttpUserSessionPayloadError> for SessionCheckerError {
|
||||
fn from(err: HttpUserSessionPayloadError) -> Self {
|
||||
Self::SessionPayload(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for SessionCheckerError {
|
||||
fn from(err: sqlx::Error) -> Self {
|
||||
Self::Sqlx(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for SessionCheckerError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
Self::OtherError(err)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix::Actor;
|
||||
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_cors_configs::cors::build_cors_config;
|
||||
use actix_cors_configs::shared_array_buffer_cors::shared_array_buffer_cors;
|
||||
use actix_helpers::middleware::banned_cidr_filter::banned_cidr_filter::BannedCidrFilter;
|
||||
@@ -71,7 +71,7 @@ use crate::billing::stripe_internal_user_lookup_impl::StripeInternalUserLookupIm
|
||||
use crate::configs::app_startup::redis_rate_limiters::configure_redis_rate_limiters;
|
||||
use crate::configs::connect_to_database::connect_to_database;
|
||||
use crate::configs::static_api_tokens::StaticApiTokenSet;
|
||||
use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use crate::http_server::middleware::error_alerting_middleware::error_alerting_middleware::ErrorAlertingMiddleware;
|
||||
use crate::http_server::middleware::pushback_filter_middleware::PushbackFilter;
|
||||
use crate::http_server::routes::add_routes::add_routes;
|
||||
@@ -93,7 +93,6 @@ use crate::util::troll_user_bans::troll_user_ban_list::TrollUserBanList;
|
||||
pub mod billing;
|
||||
pub mod configs;
|
||||
pub mod email;
|
||||
pub mod error;
|
||||
pub mod http_server;
|
||||
pub mod startup;
|
||||
pub mod state;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use actix_artcraft::sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
use actix_helpers::middleware::banned_cidr_filter::banned_cidr_set::BannedCidrSet;
|
||||
use actix_helpers::middleware::banned_ip_filter::ip_ban_list::ip_ban_list::IpBanList;
|
||||
use billing_artcraft_component::utils::artcraft_stripe_config::ArtcraftStripeConfigWithClient;
|
||||
@@ -25,7 +25,7 @@ use url_config::third_party_url_redirector::ThirdPartyUrlRedirector;
|
||||
|
||||
use crate::configs::app_startup::username_set::UsernameSet;
|
||||
use crate::configs::static_api_tokens::StaticApiTokenSet;
|
||||
use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use crate::http_server::deprecated_endpoints::categories::tts::list_fully_computed_assigned_tts_categories::list_fully_computed_assigned_tts_categories::ModelTokensByCategoryToken;
|
||||
use crate::http_server::deprecated_endpoints::leaderboard::get_leaderboard::LeaderboardInfo;
|
||||
use crate::http_server::endpoints::media_files::list::list_featured_media_files_handler::ListFeaturedMediaFilesQueryParams;
|
||||
|
||||
@@ -17,8 +17,8 @@ import { TaskQueue } from "./task-queue";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ name: "Home", href: "/" },
|
||||
// { name: "Image", href: "/create-image" },
|
||||
// { name: "Video", href: "/create-video" },
|
||||
{ name: "Image", href: "/create-image" },
|
||||
{ name: "Video", href: "/create-video" },
|
||||
{ name: "Tutorials", href: "/tutorials" },
|
||||
{ name: "News", href: "/news" },
|
||||
{ name: "FAQ", href: "/faq" },
|
||||
|
||||
@@ -11,15 +11,16 @@ const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement,
|
||||
);
|
||||
|
||||
// In development, route API through the Vite dev server origin to avoid CORS
|
||||
if (import.meta.env.DEV) {
|
||||
try {
|
||||
const origin = window.location.origin;
|
||||
StorytellerApiHostStore.getInstance().setApiSchemeAndHost(origin);
|
||||
} catch (e) {
|
||||
console.warn("Failed to set dev API host override", e);
|
||||
}
|
||||
}
|
||||
// In development, route API through the Vite dev server proxy to avoid CORS,
|
||||
// unless VITE_API_HOST is explicitly set (eg. for an unsafe browser that ignores CORS).
|
||||
//if (import.meta.env.DEV && !import.meta.env.VITE_API_HOST) {
|
||||
// try {
|
||||
// const origin = window.location.origin;
|
||||
// StorytellerApiHostStore.getInstance().setApiSchemeAndHost(origin);
|
||||
// } catch (e) {
|
||||
// console.warn("Failed to set dev API host override", e);
|
||||
// }
|
||||
//}
|
||||
|
||||
// Cache the referrer immediately — it can change or disappear on navigation.
|
||||
// Only set once so subsequent in-site navigations don't overwrite the original.
|
||||
|
||||
@@ -26,7 +26,9 @@ export class ApiManager {
|
||||
}
|
||||
|
||||
protected getApiSchemeAndHost(): string {
|
||||
return StorytellerApiHostStore.getInstance().getApiSchemeAndHost();
|
||||
const host = StorytellerApiHostStore.getInstance().getApiSchemeAndHost();
|
||||
console.log('getSchemaAndHost()', host);
|
||||
return host;
|
||||
}
|
||||
|
||||
public async fetch<B, T>(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Artcraft / Storyteller API backend
|
||||
const DEFAULT_API_HOST = "https://api.storyteller.ai";
|
||||
// Set VITE_API_HOST=http://localhost:12345 for local dev, or leave unset for production.
|
||||
//const DEFAULT_API_HOST = (import.meta as any).env?.VITE_API_HOST || "https://api.storyteller.ai";
|
||||
|
||||
const DEFAULT_API_HOST = "http://localhost:12345";
|
||||
|
||||
// NB(bt,2025-09-25): 'nx' is creating multiple copies of the library with name
|
||||
// mangling, so the singleton pattern fails to resolve to a single instance.
|
||||
|
||||
Reference in New Issue
Block a user