Compare commits

...
Author SHA1 Message Date
Brandon Thomas 0571b91928 attempt to re-connect package dependencies -- THIS WORKED 2025-09-18 19:08:26 -04:00
Brandon Thomas 2d94078db9 patch old /frontend on top of new /frontend - this works 2025-09-18 18:47:27 -04:00
Brandon Thomas f559613bff add clean modules 2025-09-18 18:24:36 -04:00
Brandon Thomas 73445fa016 attempt to merge package-lock 2025-09-18 17:44:25 -04:00
Brandon Thomas 9d9ec36341 stripe payments and subscriptions (#829) 2025-09-17 11:43:00 -04:00
BFlat 0163504aad fixed characters upload and gallery dnd + added 3d meme models (#827)
- added 6 memes models and custom thumbnails
- upload modal now has checkbox to allow character uploads
- gallery drag and drop now works properly for characters
2025-09-17 12:26:35 +07:00
bflatastic 5093361f2c added artcraft icon and updated provider priority to use model icons 2025-09-17 09:23:25 +07:00
BFlat b59909c1aa fixed inpainting upload from file being stuck (#825) 2025-09-15 14:09:52 +07:00
BFlat 9d8eedc77a Feature/dnd image prompts (#823)
- hide/show end frame input depending on the model selected
- image prompt drag and drop reordering if there's more than one image
added
2025-09-12 14:12:35 +07:00
BFlat 0e7ebd3914 updated landing page videos (#822)
- changed videos for 3d and 2d canvas
- added a video for inpainting (copy text needs to be updated)
2025-09-11 14:51:22 +07:00
bflatastic d508bf55e2 nx workspace sync 2025-09-11 14:21:15 +07:00
Tanish Manku cac0c0b188 Auto-switch to edit mode if model supports it (#818) 2025-09-10 13:50:02 +07:00
bflatastic f837f79404 fixed title disappearing on generate 2025-09-10 13:47:34 +07:00
BFlat b632d79833 image/video promptbox generation bug fixes (#821)
- make top image prompt row always show for video
- video promptbox pops a toast when reference image is empty
- description changes for ending frame section
- hid the add image button for video generation for now since that's
almost always required
2025-09-10 13:45:00 +07:00
Tanish Mankuandbflatastic d909d55e6f Support history item deletion (#819)
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2025-09-10 13:23:05 +07:00
Tanish Manku 1847533e8a Add an image removal button in history stack 2025-09-09 12:44:17 -07:00
BFlat 95894a87b9 video page update (#817)
- video prompt box now supports image prompt (starting and ending frame)
- shared image prompts section between image / video generation prompt
boxes
- video history / pending generation like text to image
- background change

- the history stack is commented out for now because backend doesnt
support it yet.
2025-09-09 19:03:26 +07:00
Tanish Manku 3ad642f150 Make mouse dragging pixel-perfect (#816) 2025-09-04 14:38:19 -07:00
bflatastic 3fb683185a console errors and log spams fixed 2025-09-04 22:32:36 +07:00
BFlat 36780e5669 ux update inpainting history (#815)
- reverted to previous ux
- added placeholder loaders for generating images (matched by frontend
subscriber id)
- can now generate multiple images at a time
- auto scrolls to top of history stack on enqueue
2025-09-03 23:05:05 +07:00
BFlat 77614e06d9 credits UI refill (#814) 2025-09-03 21:49:36 +07:00
Brandon Thomas 4c1d1007a2 icon change, metadata change (#813) 2025-08-31 13:13:58 -04:00
bflatastic 9740121012 history items to only show after enqueue succeeds 2025-08-31 11:07:48 +07:00
BFlat 27d6794d22 credits ui - topbar and settings modal (#804)
- new progress bar and progress circle component
- clicking the credits in the top opens the settings modal on the "Plans
& Credits" tab
2025-08-31 11:06:29 +07:00
Brandon Thomas 4aa91af67e trigger payments events in tauri/artcraft from the server (#805) 2025-08-30 20:34:04 -04:00
480 changed files with 14491 additions and 2553 deletions
Generated
+258 -3
View File
@@ -926,11 +926,155 @@ dependencies = [
"serde_qs 0.10.1",
"sha2 0.10.8",
"smart-default 0.6.0",
"smol_str",
"smol_str 0.1.24",
"thiserror 1.0.63",
"tokio",
]
[[package]]
name = "async-stripe"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecbddf002ad7a13d2041eadf1b234cb3f57653ffdd901a01bc3f1c65aa77440"
dependencies = [
"chrono",
"futures-util",
"hex",
"hmac 0.12.1",
"http-types",
"hyper 0.14.28",
"hyper-tls 0.5.0",
"serde",
"serde_json",
"serde_path_to_error",
"serde_qs 0.10.1",
"sha2 0.10.8",
"smart-default 0.6.0",
"smol_str 0.1.24",
"thiserror 1.0.63",
"tokio",
]
[[package]]
name = "async-stripe"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92140fa7152f9dcde7554cb1da37c7ae93b0c724db5214c754c55a9765afbe8"
dependencies = [
"async-stripe-client-core",
"async-stripe-shared",
"bytes 1.10.1",
"http-body-util",
"hyper 1.6.0",
"hyper-tls 0.6.0",
"hyper-util",
"miniserde",
"thiserror 2.0.12",
"tokio",
]
[[package]]
name = "async-stripe-billing"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c4cd0ac61e0dcb39da4f3c04b049c0d8a3a98868ca976e18ff56ecac1b6e08"
dependencies = [
"async-stripe-client-core",
"async-stripe-shared",
"async-stripe-types",
"miniserde",
"serde",
"smol_str 0.3.2",
]
[[package]]
name = "async-stripe-checkout"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b115740c799730d4caac099d48be4908d2d99611d56a874d2d3555b5d4306a97"
dependencies = [
"async-stripe-client-core",
"async-stripe-shared",
"async-stripe-types",
"miniserde",
"serde",
"smol_str 0.3.2",
]
[[package]]
name = "async-stripe-client-core"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb1f6b6693946e650a9361feb319389a1408534ec1b618cd9239f1e2e20b8668"
dependencies = [
"async-stripe-shared",
"async-stripe-types",
"bytes 1.10.1",
"futures-util",
"miniserde",
"serde",
"serde_json",
"serde_qs 0.13.0",
"thiserror 2.0.12",
"tracing",
]
[[package]]
name = "async-stripe-core"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81d0445a38170bcc3e4adf52fd80c94b70b02dc73a102afcd0bc9d6051eada18"
dependencies = [
"async-stripe-client-core",
"async-stripe-shared",
"async-stripe-types",
"miniserde",
"serde",
"smol_str 0.3.2",
]
[[package]]
name = "async-stripe-shared"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b4328e5c0deeacd7cd5a95286d4f5dce045791b30be87583dd706c2342236c3"
dependencies = [
"async-stripe-types",
"miniserde",
"serde",
"serde_json",
"smol_str 0.3.2",
]
[[package]]
name = "async-stripe-types"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985b4f00fd57ce9cbd015c6410d05dfbe5d2bf90513e20b417e70b6f022e3f88"
dependencies = [
"miniserde",
"serde",
"serde_json",
"smol_str 0.3.2",
]
[[package]]
name = "async-stripe-webhook"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f8382af92463fa8e0b86e825d7d08eecbcd8715c197f1a66173fcc18e35774"
dependencies = [
"async-stripe-shared",
"async-stripe-types",
"chrono",
"hex",
"hmac 0.12.1",
"miniserde",
"serde",
"sha2 0.10.8",
"thiserror 1.0.63",
]
[[package]]
name = "async-task"
version = "4.7.1"
@@ -1201,6 +1345,41 @@ dependencies = [
"num-traits",
]
[[package]]
name = "billing_artcraft_component"
version = "0.0.1"
dependencies = [
"actix-http",
"actix-web",
"anyhow",
"artcraft_api_defs",
"async-stripe 1.0.0-alpha.2",
"async-stripe-billing",
"async-stripe-checkout",
"async-stripe-core",
"async-stripe-shared",
"async-stripe-types",
"async-stripe-webhook",
"chrono",
"component_traits",
"enums",
"errors",
"http_server_common",
"log",
"mockall",
"mysql_queries",
"once_cell",
"reusable_types",
"serde",
"serde_derive",
"serde_json",
"sqlx",
"tokens",
"tokio",
"user_traits_component",
"workspace-build-acceleration",
]
[[package]]
name = "billing_component"
version = "0.0.1"
@@ -1208,9 +1387,11 @@ dependencies = [
"actix-http",
"actix-web",
"anyhow",
"async-stripe",
"async-stripe 0.26.0",
"async-trait",
"chrono",
"component_traits",
"enums",
"errors",
"http_server_common",
"log",
@@ -2089,6 +2270,34 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "component_traits"
version = "0.0.1"
dependencies = [
"actix-http",
"actix-web",
"anyhow",
"artcraft_api_defs",
"async-trait",
"chrono",
"enums",
"errors",
"http_server_common",
"log",
"mockall",
"mysql_queries",
"once_cell",
"reusable_types",
"serde",
"serde_derive",
"serde_json",
"sqlx",
"tokens",
"tokio",
"user_traits_component",
"workspace-build-acceleration",
]
[[package]]
name = "composite_identifiers"
version = "0.0.1"
@@ -6398,6 +6607,17 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "mini-internal"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6c74ab4f1a0c0ab045260ee4727b23c00cc17e5eff5095262d08eef8c3c8d49"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "minidom"
version = "0.16.0"
@@ -6413,6 +6633,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniserde"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08ec68bf2ad170a53a6efa92c3df7187968d6e475fe7a935725868154074ca0f"
dependencies = [
"itoa 1.0.11",
"mini-internal",
"ryu",
]
[[package]]
name = "miniz_oxide"
version = "0.7.2"
@@ -9528,6 +9759,17 @@ dependencies = [
"thiserror 1.0.63",
]
[[package]]
name = "serde_qs"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6"
dependencies = [
"percent-encoding",
"serde",
"thiserror 1.0.63",
]
[[package]]
name = "serde_repr"
version = "0.1.19"
@@ -9904,6 +10146,16 @@ dependencies = [
"serde",
]
[[package]]
name = "smol_str"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d"
dependencies = [
"borsh",
"serde",
]
[[package]]
name = "socket2"
version = "0.5.10"
@@ -10292,9 +10544,11 @@ dependencies = [
"actix_helpers",
"anyhow",
"artcraft_api_defs",
"async-stripe",
"async-stripe 0.26.0",
"async-stripe 0.41.0",
"async-trait",
"base64 0.22.1",
"billing_artcraft_component",
"billing_component",
"bootstrap",
"bucket_paths",
@@ -10302,6 +10556,7 @@ dependencies = [
"chrono",
"cloud_storage",
"collections",
"component_traits",
"composite_identifiers",
"config",
"cookies",
+37 -32
View File
@@ -6,6 +6,7 @@ resolver = "2"
members = [
# API clients for external services
# These might be custom implementations or wrappers over other client libraries.
"crates/api_clients/artcraft_api_defs", # TODO: api_clients/{artcraft}/{api_defs,client}
"crates/api_clients/cloud_storage",
"crates/api_clients/fal_client",
"crates/api_clients/google_drive_common",
@@ -97,7 +98,6 @@ members = [
#"crates/schema/database/sqlite_queries",
# Schema (public): common strongly-typed tokens, ids, payloads, etc.
"crates/schema/public/api/artcraft_api_defs",
"crates/schema/public/composite_identifiers",
"crates/schema/public/enums",
"crates/schema/public/enums_public",
@@ -119,6 +119,8 @@ members = [
# Service Plugins: complete subsystems that can be plugged into services (users, billing, etc.)
"crates/service/plugins/billing",
"crates/service/plugins/billing_artcraft",
"crates/service/plugins/component_traits",
"crates/service/plugins/inference_workloads",
"crates/service/plugins/user_traits_component",
@@ -176,6 +178,40 @@ publish = false
#
workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
# Internal libs
# Setting paths here ensures we don't need to do so in every crate's Cargo.toml
# It makes refactoring much easier.
artcraft_api_defs = { path = "crates/api_clients/artcraft_api_defs" }
build_metadata = { path = "crates/lib/build_metadata" }
cloud_storage = { path = "crates/api_clients/cloud_storage" }
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
component_traits = { path = "crates/service/plugins/component_traits" }
cookie_store = { path = "crates/lib/cookie_store" }
crockford = { path = "crates/lib/data/crockford" }
enums = { path = "crates/schema/public/enums" }
enums_public = { path = "crates/schema/public/enums_public" }
errors = { path = "crates/lib/errors" }
fal_client = { path = "crates/api_clients/fal_client" }
file_to_base64_url = { path = "crates/lib/files/file_to_base64_url" }
filesys = { path = "crates/lib/files/filesys" }
idempotency = { path = "crates/lib/idempotency" }
images = { path = "crates/lib/files/images" }
jwt_light = { path = "crates/lib/jwt_light" }
memory_caching = { path = "crates/lib/caching/memory_caching" }
memory_store = { path = "crates/lib/caching/memory_store" }
midjourney_client = { path = "crates/api_clients/midjourney_client" }
mimetypes = { path = "crates/lib/files/mimetypes" }
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
shared_service_components = { path = "crates/lib/shared_service_components" }
sqlite_tasks = { path = "crates/schema/database/sqlite_tasks" }
storyteller_client = { path = "crates/api_clients/storyteller_client" }
subprocess_common = { path = "crates/lib/subprocess_common" }
test_data = { path = "crates/lib/test_data" }
testing = { path = "crates/lib/testing" }
tokens = { path = "crates/schema/public/tokens" }
web_base64 = { path = "crates/lib/data/web_base64" }
# External libs
# Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace
# If a crate needs to use an older/newer version, the dependency should be managed in that crate's own Cargo.toml
@@ -219,34 +255,3 @@ thiserror = "2.0.12"
tokio = { version = "1.45.1", features = ["macros"] }
toml = "0.8.22"
url = { version = "2.5.4", features = ["serde"] }
# Internal libs
artcraft_api_defs = { path = "crates/schema/public/api/artcraft_api_defs" }
build_metadata = { path = "crates/lib/build_metadata" }
cloud_storage = { path = "crates/api_clients/cloud_storage" }
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
cookie_store = { path = "crates/lib/cookie_store" }
crockford = { path = "crates/lib/data/crockford" }
enums = { path = "crates/schema/public/enums" }
enums_public = { path = "crates/schema/public/enums_public" }
errors = { path = "crates/lib/errors" }
fal_client = { path = "crates/api_clients/fal_client" }
file_to_base64_url = { path = "crates/lib/files/file_to_base64_url" }
filesys = { path = "crates/lib/files/filesys" }
idempotency = { path = "crates/lib/idempotency" }
images = { path = "crates/lib/files/images" }
jwt_light = { path = "crates/lib/jwt_light" }
memory_caching = { path = "crates/lib/caching/memory_caching" }
memory_store = { path = "crates/lib/caching/memory_store" }
midjourney_client = { path = "crates/api_clients/midjourney_client" }
mimetypes = { path = "crates/lib/files/mimetypes" }
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
shared_service_components = { path = "crates/lib/shared_service_components" }
sqlite_tasks = { path = "crates/schema/database/sqlite_tasks" }
storyteller_client = { path = "crates/api_clients/storyteller_client" }
subprocess_common = { path = "crates/lib/subprocess_common" }
test_data = { path = "crates/lib/test_data" }
testing = { path = "crates/lib/testing" }
tokens = { path = "crates/schema/public/tokens" }
web_base64 = { path = "crates/lib/data/web_base64" }
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_subscriptions DROP COLUMN maybe_stripe_invoice_is_paid;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_subscriptions
ADD COLUMN maybe_stripe_invoice_is_paid BOOLEAN DEFAULT NULL
AFTER maybe_stripe_subscription_status;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE wallets;
@@ -0,0 +1,66 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE wallets (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC)
token VARCHAR(32) NOT NULL,
-- If we host multiple sites with distinct categories, this will enable us to
-- segregate wallets. This is effectively a wallet namespace.
-- NB: Uses the `PaymentsNamespace` enum, which is also shared with subscriptions.
wallet_namespace VARCHAR(32) NOT NULL,
-- Who owns the wallet
-- In the future, we might allow other users to use the same wallet,
-- but the wallet would still have an owner.
-- For now, we'll keep this 1:1 with users, but we may want 1:n in the future.
owner_user_token VARCHAR(32) NOT NULL,
-- The durable credits bucket.
-- This bucket is persistent and is refilled by user credit purchases.
-- Max value unsigned: 4,294,967,295 (U32)
banked_credits INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- The monthly credits bucket.
-- This bucket is ephemeral and is refilled monthly.
-- Max value unsigned: 4,294,967,295 (U32)
monthly_credits INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Whether the stripe subscription is active.
-- subscription_is_active BOOLEAN NOT NULL DEFAULT FALSE,
-- Whether the stripe subscription is paid.
-- If the subscription is not paid, we should not allow usage of monthly credits.
-- subscription_is_paid BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== VECTOR CLOCK ==========
-- Incremented with every update.
version INT NOT NULL DEFAULT 0,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== PAYMENTS AND SUBSCRIPTIONS TIMESTAMPS ==========
-- When the user last purchased banked (durable) credits.
-- banked_credits_last_purchased_at TIMESTAMP DEFAULT NULL,
-- When we last refilled the monthly bucket.
-- This has to do with subscription logic.
-- monthly_wallet_refilled_at TIMESTAMP DEFAULT NULL,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (wallet_namespace, owner_user_token), -- For now
INDEX idx_wallet_namespace (wallet_namespace),
INDEX idx_owner_user_token (owner_user_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE wallet_ledger_entries;
@@ -0,0 +1,52 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE wallet_ledger_entries (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC)
token VARCHAR(32) NOT NULL,
-- Which wallet this entry corresponds to.
wallet_token VARCHAR(32) NOT NULL,
-- The type of ledger entry this is.
entry_type VARCHAR(32) NOT NULL,
-- A record we can look up to attribute the credit or deduction against
-- This could be an internal entity token (prompt_token, job token) or an
-- external token (stripe event_id, stripe invoice.id, etc.)
-- Length is VARCHAR(255) to accommodate Stripe IDs.
maybe_entity_ref VARCHAR(255) DEFAULT NULL,
-- Signed integer that shows the delta of credits for this event.
-- This combines impacts to both banked_credits and monthly_credits.
-- This is less authoritative than the before/after fields, but is useful
credits_delta INTEGER NOT NULL DEFAULT 0,
-- Balance of banked credits before the event.
-- Max value unsigned: 4,294,967,295 (U32)
banked_credits_before INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of banked credits after the event.
banked_credits_after INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of monthly credits before the event.
-- Max value unsigned: 4,294,967,295 (U32)
monthly_credits_before INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of monthly credits after the event.
monthly_credits_after INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY index_wallet_token (wallet_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_subscriptions DROP COLUMN maybe_stripe_billing_cycle_anchor;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_subscriptions
ADD COLUMN maybe_stripe_billing_cycle_anchor TIMESTAMP DEFAULT NULL
AFTER maybe_stripe_invoice_is_paid;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE stripe_webhook_event_logs DROP COLUMN maybe_internal_entity_token;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE stripe_webhook_event_logs
ADD COLUMN maybe_internal_entity_token VARCHAR(32) DEFAULT NULL
AFTER maybe_user_token;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE user_stripe_customer_links;
@@ -0,0 +1,36 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- 1:1 links between a user and a stripe customer (per payments namespace)
-- We can hard delete records if we have to.
-- If users need a new stripe customer ID, we need to hard delete the existing record.
CREATE TABLE user_stripe_customer_links (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Composite primary key
user_token VARCHAR(32) NOT NULL,
-- Composite primary key
-- If we host multiple sites with distinct categories, this will enable us to
-- segregate accounts.
-- NB: Uses the `PaymentsNamespace` enum, which is also shared with subscriptions.
payments_namespace VARCHAR(32) NOT NULL,
-- Unique key. (We can loosen to `stripe_customer_id, wallet_namespace` later if
-- we want to share billing.)
stripe_customer_id VARCHAR(255) NOT NULL,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (user_token, payments_namespace),
-- UNIQUE KEY (stripe_customer_id, payments_namespace)
UNIQUE KEY (stripe_customer_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,66 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE stripe_webhook_event_logs (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- ========== STRIPE DATA ==========
-- You can safely assume object IDs we generate will never exceed 255 characters,
-- but you should be able to handle IDs of up to that length. If for example you’re using MySQL,
-- you should store IDs in a VARCHAR(255) COLLATE utf8_bin column (the COLLATE configuration ensures
-- case-sensitivity in lookups).
-- https://stackoverflow.com/a/61809494
stripe_event_id VARCHAR(255) NOT NULL,
-- The type of webhook event
stripe_event_type VARCHAR(255) NOT NULL,
-- The id of the entity that was the subject of the event.
-- We don't always extract this.
maybe_stripe_event_entity_id VARCHAR(255) DEFAULT NULL,
-- The id of the entity that was the subject of the event.
-- We don't always extract this.
maybe_stripe_customer_id VARCHAR(255) DEFAULT NULL,
-- Whether this is in production or test mode in Stripe.
-- This is controlled by which API keys are used.
-- "livemode"=true in production.
stripe_is_production BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== INTERNAL SYSTEM METADATA ==========
-- Whether we handled the event with any sort of action
action_was_taken BOOLEAN NOT NULL DEFAULT FALSE,
-- Whether we should ignore any future retries by Stripe replaying the event.
-- This helps us treat events with idempotency.
should_ignore_retry BOOLEAN NOT NULL DEFAULT FALSE,
-- Some event types may attach a user token
maybe_user_token VARCHAR(32) DEFAULT NULL,
-- An internal optional entity token this resolves to.
-- eg. `wallets.token` or `user_subscriptions.token`.
maybe_internal_entity_token VARCHAR(32) DEFAULT NULL,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- When the event was created on Stripe's end
stripe_event_created_at TIMESTAMP NOT NULL,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (stripe_event_id),
KEY index_stripe_event_type (stripe_event_type),
KEY index_maybe_stripe_event_entity_id (maybe_stripe_event_entity_id),
KEY index_maybe_stripe_customer_id (maybe_stripe_customer_id),
KEY fk_maybe_user_token (maybe_user_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,36 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- 1:1 links between a user and a stripe customer (per payments namespace)
-- We can hard delete records if we have to.
-- If users need a new stripe customer ID, we need to hard delete the existing record.
CREATE TABLE user_stripe_customer_links (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Composite primary key
user_token VARCHAR(32) NOT NULL,
-- Composite primary key
-- If we host multiple sites with distinct categories, this will enable us to
-- segregate accounts.
-- NB: Uses the `PaymentsNamespace` enum, which is also shared with subscriptions.
payments_namespace VARCHAR(32) NOT NULL,
-- Unique key. (We can loosen to `stripe_customer_id, wallet_namespace` later if
-- we want to share billing.)
stripe_customer_id VARCHAR(255) NOT NULL,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (user_token, payments_namespace),
-- UNIQUE KEY (stripe_customer_id, payments_namespace)
UNIQUE KEY (stripe_customer_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -53,6 +53,18 @@ CREATE TABLE user_subscriptions (
-- Subscription object status enum as string
maybe_stripe_subscription_status VARCHAR(32) DEFAULT NULL,
-- Primarily managed by the `invoice.paid` and `invoice.payment_failed` webhooks.
-- NB: Not applicable to FakeYou subscriptions!
maybe_stripe_invoice_is_paid BOOLEAN DEFAULT NULL,
-- Which day of the month, or month of the year, etc. to anchor the subscription to.
-- See docs on the subscription object.
-- Stripe:
-- "The reference point that aligns future billing cycle dates. It sets the day
-- of week for week intervals, the day of month for month and year intervals, and
-- the month of year for year intervals. The timestamp is in UTC format."
maybe_stripe_billing_cycle_anchor TIMESTAMP DEFAULT NULL,
maybe_stripe_is_production BOOLEAN DEFAULT NULL,
-- ========== VECTOR CLOCK ==========
@@ -64,6 +76,8 @@ CREATE TABLE user_subscriptions (
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- NB(bt, 2025-09-16): I don't think we write to the soft deleted_at field for user_subscriptions.
deleted_at TIMESTAMP DEFAULT NULL,
-- ========== SUBSCRIPTION TIMESTAMPS ==========
@@ -72,7 +86,10 @@ CREATE TABLE user_subscriptions (
-- This may predate the Stripe object `created` timestamp due to backdating.
subscription_start_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- We'll use this to determine if the subscription is active.
-- Our synthetic "expire" date. Moved forward for active subscriptions,
-- backdated for canceled subscriptions.
--
-- TODO: We'll (maybe?) use this to determine if the subscription is active.
--
-- Always compare against MySQL's clock rather than the app's clock so that we
-- don't get weird clock skew behaviors across multiple requests.
@@ -82,8 +99,12 @@ CREATE TABLE user_subscriptions (
subscription_expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Billing periods.
-- Maybe useful for debugging.
-- Stripe API: subscription.items[0].data.current_period_start
current_billing_period_start_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Stripe API: subscription.items[0].data.current_period_end
-- current_period_end is when the subscription renews (or cancels, if the
-- subscription is set to cancel).
current_billing_period_end_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Subscription cancellation (future and past)
@@ -0,0 +1,52 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE wallet_ledger_entries (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC)
token VARCHAR(32) NOT NULL,
-- Which wallet this entry corresponds to.
wallet_token VARCHAR(32) NOT NULL,
-- The type of ledger entry this is.
entry_type VARCHAR(16) NOT NULL,
-- A record we can look up to attribute the credit or deduction against
-- This could be an internal entity token (prompt_token, job token) or an
-- external token (stripe event_id, stripe invoice.id, etc.)
-- Length is VARCHAR(255) to accommodate Stripe IDs.
maybe_entity_ref VARCHAR(255) DEFAULT NULL,
-- Signed integer that shows the delta of credits for this event.
-- This combines impacts to both banked_credits and monthly_credits.
-- This is less authoritative than the before/after fields, but is useful
credits_delta INTEGER NOT NULL DEFAULT 0,
-- Balance of banked credits before the event.
-- Max value unsigned: 4,294,967,295 (U32)
banked_credits_before INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of banked credits after the event.
banked_credits_after INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of monthly credits before the event.
-- Max value unsigned: 4,294,967,295 (U32)
monthly_credits_before INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of monthly credits after the event.
monthly_credits_after INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY index_wallet_token (wallet_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,66 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE wallets (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC)
token VARCHAR(32) NOT NULL,
-- If we host multiple sites with distinct categories, this will enable us to
-- segregate wallets. This is effectively a wallet namespace.
-- NB: Uses the `PaymentsNamespace` enum, which is also shared with subscriptions.
wallet_namespace VARCHAR(32) NOT NULL,
-- Who owns the wallet
-- In the future, we might allow other users to use the same wallet,
-- but the wallet would still have an owner.
-- For now, we'll keep this 1:1 with users, but we may want 1:n in the future.
owner_user_token VARCHAR(32) NOT NULL,
-- The durable credits bucket.
-- This bucket is persistent and is refilled by user credit purchases.
-- Max value unsigned: 4,294,967,295 (U32)
banked_credits INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- The monthly credits bucket.
-- This bucket is ephemeral and is refilled monthly.
-- Max value unsigned: 4,294,967,295 (U32)
monthly_credits INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Whether the stripe subscription is active.
-- subscription_is_active BOOLEAN NOT NULL DEFAULT FALSE,
-- Whether the stripe subscription is paid.
-- If the subscription is not paid, we should not allow usage of monthly credits.
-- subscription_is_paid BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== VECTOR CLOCK ==========
-- Incremented with every update.
version INT NOT NULL DEFAULT 0,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== PAYMENTS AND SUBSCRIPTIONS TIMESTAMPS ==========
-- When the user last purchased banked (durable) credits.
-- banked_credits_last_purchased_at TIMESTAMP DEFAULT NULL,
-- When we last refilled the monthly bucket.
-- This has to do with subscription logic.
-- monthly_wallet_refilled_at TIMESTAMP DEFAULT NULL,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (wallet_namespace, owner_user_token), -- For now
INDEX idx_wallet_namespace (wallet_namespace),
INDEX idx_owner_user_token (owner_user_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,20 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GetSessionCreditsResponse {
pub success: bool,
/// Any free credits the user might have
/// We might support daily free credits.
pub free_credits: u64,
/// Any monthly refilling credits the user might have
pub monthly_credits: u64,
/// Any banked credits the user might have.
pub banked_credits: u64,
/// All the credit amounts added together.
pub sum_total_credits: u64,
}
@@ -0,0 +1 @@
pub mod get_session_credits;
@@ -0,0 +1,8 @@
pub mod common;
pub mod credits;
pub mod generate;
pub mod jobs;
pub mod media_file;
pub mod prompts;
pub mod stripe_artcraft;
pub mod subscriptions;
@@ -0,0 +1,17 @@
use enums::common::artcraft_credits_pack_slug::ArtcraftCreditsPackSlug;
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CREATE_CREDITS_PACK_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/checkout/credits_pack";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateCreditsPackCheckoutRequest {
pub credits_pack: Option<ArtcraftCreditsPackSlug>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateCreditsPackCheckoutResponse {
pub success: bool,
pub stripe_checkout_redirect_url: String,
}
@@ -0,0 +1,30 @@
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/checkout/subscription";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateSubscriptionCheckoutRequest {
/// The (non-Stripe) internal identifier for the product or subscription.
/// This will be translated into a Stripe identifier.
pub plan: Option<ArtcraftSubscriptionSlug>,
pub cadence: Option<PlanBillingCadence>,
}
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
pub enum PlanBillingCadence {
#[serde(rename = "monthly")]
Monthly,
#[serde(rename = "yearly")]
Yearly,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateSubscriptionCheckoutResponse {
pub success: bool,
pub stripe_checkout_redirect_url: String,
}
@@ -0,0 +1,17 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CUSTOMER_PORTAL_CANCEL_PLAN_URL_PATH: &str = "/v1/stripe_artcraft/portal/cancel_plan";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalCancelPlanRequest {
// TODO: Not sure if this is needed
pub portal_config_id: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalCancelPlanResponse {
pub success: bool,
pub stripe_portal_url: String,
}
@@ -0,0 +1,17 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CUSTOMER_PORTAL_MANAGE_PLAN_URL_PATH: &str = "/v1/stripe_artcraft/portal/manage_plan";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalManagePlanRequest {
// TODO: Not sure if this is needed
pub portal_config_id: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalManagePlanResponse {
pub success: bool,
pub stripe_portal_url: String,
}
@@ -0,0 +1,37 @@
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CUSTOMER_PORTAL_SWITCH_PLAN_URL_PATH: &str = "/v1/stripe_artcraft/portal/switch_plan";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalSwitchPlanRequest {
// TODO: Not sure if this is needed
pub portal_config_id: Option<String>,
/// REQUIRED
/// The plan we're switching to.
pub plan: Option<ArtcraftSubscriptionSlug>,
/// REQUIRED
/// The cadence for the plan billing.
pub cadence: Option<PlanBillingCadenceConfirmation>,
}
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
pub enum PlanBillingCadenceConfirmation {
#[serde(rename = "monthly")]
Monthly,
#[serde(rename = "yearly")]
Yearly,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalSwitchPlanResponse {
pub success: bool,
pub stripe_portal_url: String,
}
@@ -0,0 +1,17 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CUSTOMER_PORTAL_UPDATE_PAYMENT_METHOD_URL_PATH: &str = "/v1/stripe_artcraft/portal/update_payment_method";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalUpdatePaymentMethodRequest {
// TODO: Not sure if this is needed
pub portal_config_id: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalUpdatePaymentMethodResponse {
pub success: bool,
pub stripe_portal_url: String,
}
@@ -0,0 +1,6 @@
pub mod create_credits_pack_checkout;
pub mod create_subscription_checkout;
pub mod customer_portal_cancel_plan;
pub mod customer_portal_manage_plan;
pub mod customer_portal_switch_plan;
pub mod customer_portal_update_payment_method;
@@ -0,0 +1,37 @@
use chrono::{DateTime, Utc};
use enums::common::payments_namespace::PaymentsNamespace;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::user_subscriptions::UserSubscriptionToken;
use utoipa::ToSchema;
// TODO: Maybe make "artcraft_subscription" or "fakeyou_subscription" with strongly typed slugs?
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GetSessionSubscriptionResponse {
pub success: bool,
/// Information about the user's subscription, if any.
pub active_subscription: Option<SubscriptionInfo>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct SubscriptionInfo {
/// Unique token for the subscription.
pub subscription_token: UserSubscriptionToken,
/// Namespace for the subscription.
pub namespace: PaymentsNamespace,
/// Local key for the subscription.
pub product_slug: String,
/// If the subscription is currently active, this is the next billing date.
pub next_bill_at: Option<DateTime<Utc>>,
/// If the subscription is set to cancel, this is the end date of the subscription.
pub subscription_end_at: Option<DateTime<Utc>>,
///// When the current billing period ends (either
///// auto-renew/rebill date or auto-cancel/cancellation date).
//pub current_billing_period_end_at: DateTime<Utc>,
}
@@ -0,0 +1 @@
pub mod get_session_subscription;
@@ -0,0 +1,27 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_get_request::basic_json_get_request;
use artcraft_api_defs::credits::get_session_credits::GetSessionCreditsResponse;
use enums::common::payments_namespace::PaymentsNamespace;
pub async fn get_session_credits(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
payments_namespace: PaymentsNamespace
) -> Result<GetSessionCreditsResponse, StorytellerError> {
let url_path = get_url_path(payments_namespace);
Ok(basic_json_get_request(
api_host,
&url_path,
maybe_creds,
).await?)
}
fn get_url_path(payments_namespace: PaymentsNamespace) -> String {
let payments_namespace = payments_namespace.to_str();
format!("/v1/credits/namespace/{}", payments_namespace)
}
@@ -0,0 +1 @@
pub mod get_session_credits;
@@ -11,6 +11,9 @@ pub enum ApiError {
/// 401. The request was not authorized.
Unauthorized(String),
// 402. Payment required.
PaymentRequired(String),
/// 403. The request was forbidden.
Forbidden(String),
@@ -56,6 +59,7 @@ impl Display for ApiError {
match self {
// Server response code errors
ApiError::InvalidRequest(msg) => write!(f, "Invalid request: {}", msg),
ApiError::PaymentRequired(msg) => write!(f, "Payment required: {}", msg),
ApiError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
ApiError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
ApiError::NotFound(msg) => write!(f, "Not found: {}", msg),
@@ -80,6 +84,13 @@ impl Display for ApiError {
impl From<reqwest::Error> for ApiError {
fn from(error: reqwest::Error) -> Self {
if let Some(status) = error.status() {
let status = status.as_u16();
match status {
402 => return ApiError::PaymentRequired(error.to_string()),
_ => {} // NB: Fallthrough.
}
}
if error.is_timeout() {
ApiError::Timeout(error.to_string())
} else if error.is_connect() {
@@ -1,10 +1,13 @@
//! An API client for Storyteller / FakeYou
pub mod credentials;
pub mod credits;
pub mod error;
pub mod generate;
pub mod jobs;
pub mod media_files;
pub mod prompts;
pub mod recipes;
pub mod stripe_artcraft;
pub mod subscriptions;
pub mod utils;
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::stripe_artcraft::create_credits_pack_checkout::{StripeArtcraftCreateCreditsPackCheckoutRequest, StripeArtcraftCreateCreditsPackCheckoutResponse, CREATE_CREDITS_PACK_CHECKOUT_URL_PATH};
pub async fn create_credits_pack_checkout(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: StripeArtcraftCreateCreditsPackCheckoutRequest,
) -> Result<StripeArtcraftCreateCreditsPackCheckoutResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
CREATE_CREDITS_PACK_CHECKOUT_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::stripe_artcraft::create_subscription_checkout::{StripeArtcraftCreateSubscriptionCheckoutRequest, StripeArtcraftCreateSubscriptionCheckoutResponse, CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH};
pub async fn create_subscription_checkout(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: StripeArtcraftCreateSubscriptionCheckoutRequest,
) -> Result<StripeArtcraftCreateSubscriptionCheckoutResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::stripe_artcraft::customer_portal_cancel_plan::{StripeArtcraftCustomerPortalCancelPlanRequest, StripeArtcraftCustomerPortalCancelPlanResponse, CUSTOMER_PORTAL_CANCEL_PLAN_URL_PATH};
pub async fn customer_portal_cancel_plan(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: StripeArtcraftCustomerPortalCancelPlanRequest,
) -> Result<StripeArtcraftCustomerPortalCancelPlanResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
CUSTOMER_PORTAL_CANCEL_PLAN_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::stripe_artcraft::customer_portal_manage_plan::{StripeArtcraftCustomerPortalManagePlanRequest, StripeArtcraftCustomerPortalManagePlanResponse, CUSTOMER_PORTAL_MANAGE_PLAN_URL_PATH};
pub async fn customer_portal_manage_plan(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: StripeArtcraftCustomerPortalManagePlanRequest,
) -> Result<StripeArtcraftCustomerPortalManagePlanResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
CUSTOMER_PORTAL_MANAGE_PLAN_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::stripe_artcraft::customer_portal_switch_plan::{StripeArtcraftCustomerPortalSwitchPlanRequest, StripeArtcraftCustomerPortalSwitchPlanResponse, CUSTOMER_PORTAL_SWITCH_PLAN_URL_PATH};
pub async fn customer_portal_switch_plan(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: StripeArtcraftCustomerPortalSwitchPlanRequest,
) -> Result<StripeArtcraftCustomerPortalSwitchPlanResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
CUSTOMER_PORTAL_SWITCH_PLAN_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::stripe_artcraft::customer_portal_update_payment_method::{StripeArtcraftCustomerPortalUpdatePaymentMethodRequest, StripeArtcraftCustomerPortalUpdatePaymentMethodResponse, CUSTOMER_PORTAL_UPDATE_PAYMENT_METHOD_URL_PATH};
pub async fn customer_portal_update_payment_method(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: StripeArtcraftCustomerPortalUpdatePaymentMethodRequest,
) -> Result<StripeArtcraftCustomerPortalUpdatePaymentMethodResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
CUSTOMER_PORTAL_UPDATE_PAYMENT_METHOD_URL_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,6 @@
pub mod create_credits_pack_checkout;
pub mod create_subscription_checkout;
pub mod customer_portal_cancel_plan;
pub mod customer_portal_manage_plan;
pub mod customer_portal_switch_plan;
pub mod customer_portal_update_payment_method;
@@ -0,0 +1,28 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_get_request::basic_json_get_request;
use artcraft_api_defs::subscriptions::get_session_subscription::GetSessionSubscriptionResponse;
use enums::common::payments_namespace::PaymentsNamespace;
use log::debug;
pub async fn get_session_subscription(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
payments_namespace: PaymentsNamespace
) -> Result<GetSessionSubscriptionResponse, StorytellerError> {
let url_path = get_url_path(api_host, payments_namespace);
debug!("Requesting {:?}", &url_path);
Ok(basic_json_get_request(
api_host,
&url_path,
maybe_creds,
).await?)
}
fn get_url_path(api_host: &ApiHost, payments_namespace: PaymentsNamespace) -> String {
let payments_namespace = payments_namespace.to_str();
format!("/v1/subscriptions/namespace/{}", payments_namespace)
}
@@ -0,0 +1 @@
pub mod get_session_subscription;
@@ -38,6 +38,7 @@ pub async fn filter_bad_response(response: reqwest::Response) -> Result<reqwest:
match status_code {
STATUS_401_UNAUTHORIZED => Err(ApiError::Unauthorized(response_body)),
STATUS_402_PAYMENT_REQUIRED => Err(ApiError::PaymentRequired(response_body)),
STATUS_403_FORBIDDEN => Err(ApiError::Forbidden(response_body)),
STATUS_404_NOT_FOUND => Err(ApiError::NotFound(response_body)),
STATUS_429_TOO_MANY_REQUESTS => Err(ApiError::TooManyRequests(response_body)),
@@ -1,5 +1,6 @@
pub const STATUS_401_UNAUTHORIZED : u16 = 401u16;
pub const STATUS_402_PAYMENT_REQUIRED : u16 = 402u16;
pub const STATUS_403_FORBIDDEN : u16 = 403u16;
pub const STATUS_404_NOT_FOUND : u16 = 404u16;
@@ -15,6 +16,7 @@ mod tests {
#[test]
pub fn test_codes() {
assert_eq!(STATUS_401_UNAUTHORIZED, 401);
assert_eq!(STATUS_402_PAYMENT_REQUIRED, 402);
assert_eq!(STATUS_403_FORBIDDEN, 403);
assert_eq!(STATUS_404_NOT_FOUND, 404);
assert_eq!(STATUS_429_TOO_MANY_REQUESTS, 429);
@@ -0,0 +1,57 @@
use crate::core::commands::enqueue::generate_error::{BillingIssueReason, BillingProvider, GenerateError, ProviderFailureReason};
use crate::core::events::functional_events::show_provider_billing_modal_event::ShowProviderBillingModalEvent;
use enums::common::generation_provider::GenerationProvider;
use log::warn;
use storyteller_client::error::api_error::ApiError;
use storyteller_client::error::storyteller_error::StorytellerError;
use tauri::AppHandle;
pub async fn maybe_notify_frontend_of_billing_errors(
app: &AppHandle,
error: &GenerateError,
) {
match error {
GenerateError::BillingIssue(reason) => {
billing_error(app, reason);
}
GenerateError::ProviderFailure(reason) => {
provider_billing_error(app, reason);
}
_ => {
// Do nothing for other types of errors
}
}
}
fn billing_error(
app: &AppHandle,
reason: &BillingIssueReason,
) {
let provider = match reason.provider {
BillingProvider::Fal => GenerationProvider::Fal,
BillingProvider::Midjourney => GenerationProvider::Midjourney,
BillingProvider::Sora => GenerationProvider::Sora,
BillingProvider::Storyteller => GenerationProvider::Artcraft,
};
warn!("Billing issue with: {:?}", provider);
ShowProviderBillingModalEvent::send_for_provider(provider, app);
}
fn provider_billing_error(
app: &AppHandle,
error: &ProviderFailureReason,
) {
let provider;
match error {
ProviderFailureReason::StorytellerError(StorytellerError::Api(ApiError::PaymentRequired(reason))) => {
warn!("Billing issue with Artcraft/Storyteller: {}", reason);
provider = GenerationProvider::Artcraft;
}
_ => {
return;
}
}
ShowProviderBillingModalEvent::send_for_provider(provider, app);
}
@@ -0,0 +1,2 @@
pub mod notify_frontend_of_errors;
mod maybe_notify_frontend_of_billing_errors;
@@ -0,0 +1,10 @@
use crate::core::commands::enqueue::common::maybe_notify_frontend_of_billing_errors::maybe_notify_frontend_of_billing_errors;
use crate::core::commands::enqueue::generate_error::GenerateError;
use tauri::AppHandle;
pub async fn notify_frontend_of_errors(
app: &AppHandle,
errors: &GenerateError,
) {
maybe_notify_frontend_of_billing_errors(app, errors).await;
}
@@ -0,0 +1,166 @@
use crate::core::artcraft_error::ArtcraftError;
use base64::DecodeError;
use errors::AnyhowError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use midjourney_client::error::midjourney_error::MidjourneyError;
use openai_sora_client::sora_error::SoraError;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum GenerateError {
BadInput(BadInputReason),
MissingCredentials(MissingCredentialsReason),
ProviderFailure(ProviderFailureReason),
/// We couldn't find a provider to dispatch the request to.
NoProviderAvailable,
/// There was a billing, credits, or payments issue.
BillingIssue(BillingIssueReason),
/// The feature is not yet implemented.
NotYetImplemented(String),
// Misc error buckets
AnyhowError(AnyhowError),
DecodeError(DecodeError),
IoError(std::io::Error),
}
#[derive(Debug)]
pub enum BadInputReason {
Base64DecodeError,
BothImageMaskMediaTokenAndBytesSupplied,
CannotDetermineImageMimeType,
InvalidNumberOfInputImages {
provided: u32,
min: u32,
max: u32,
},
InvalidNumberOfRequestedImages {
requested: u32,
min: u32,
max: u32,
},
NoModelSpecified,
RequiredSourceImageMaskNotProvided,
RequiredSourceImageNotProvided,
WrongImageArguments(String),
}
#[derive(Debug)]
pub enum MissingCredentialsReason {
NeedsFalApiKey,
NeedsMidjourneyCredentials,
NeedsMidjourneyUserId,
NeedsMidjourneyUserInfo,
NeedsSoraCredentials,
NeedsStorytellerCredentials,
}
#[derive(Debug)]
pub struct BillingIssueReason {
pub provider: BillingProvider,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum BillingProvider {
Fal,
Midjourney,
Sora,
Storyteller,
}
#[derive(Debug)]
pub enum ProviderFailureReason {
Fal(FalErrorPlus),
MidjourneyError(MidjourneyError),
/// NB: The midjourney client doesn't categorize all errors, so we have to do so on our end.
MidjourneyJobEnqueueFailed,
SoraError(SoraError),
StorytellerError(StorytellerError),
}
impl GenerateError {
//
// Bad input
//
pub fn both_image_mask_media_token_and_bytes_supplied() -> Self {
Self::BadInput(BadInputReason::BothImageMaskMediaTokenAndBytesSupplied)
}
pub fn no_model_specified() -> Self {
Self::BadInput(BadInputReason::NoModelSpecified)
}
pub fn required_source_image_mask_not_provided() -> Self {
Self::BadInput(BadInputReason::RequiredSourceImageMaskNotProvided)
}
pub fn required_source_image_not_provided() -> Self {
Self::BadInput(BadInputReason::RequiredSourceImageNotProvided)
}
//
// Missing credentials
//
pub fn needs_fal_api_key() -> Self {
Self::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey)
}
pub fn needs_midjourney_credentials() -> Self {
Self::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyCredentials)
}
pub fn needs_sora_credentials() -> Self {
Self::MissingCredentials(MissingCredentialsReason::NeedsSoraCredentials)
}
pub fn needs_storyteller_credentials() -> Self {
Self::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials)
}
}
impl From<AnyhowError> for GenerateError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<ArtcraftError> for GenerateError {
fn from(value: ArtcraftError) -> Self {
match value {
ArtcraftError::AnyhowError(e) => Self::AnyhowError(e),
ArtcraftError::DecodeError(e) => Self::DecodeError(e),
ArtcraftError::IoError(e) => Self::IoError(e),
ArtcraftError::StorytellerError(e) => Self::ProviderFailure(ProviderFailureReason::StorytellerError(e)),
}
}
}
impl From<FalErrorPlus> for GenerateError {
fn from(value: FalErrorPlus) -> Self {
Self::ProviderFailure(ProviderFailureReason::Fal(value))
}
}
impl From<MidjourneyError> for GenerateError {
fn from(value: MidjourneyError) -> Self {
Self::ProviderFailure(ProviderFailureReason::MidjourneyError(value))
}
}
impl From<SoraError> for GenerateError {
fn from(value: SoraError) -> Self {
Self::ProviderFailure(ProviderFailureReason::SoraError(value))
}
}
impl From<StorytellerError> for GenerateError {
fn from(value: StorytellerError) -> Self {
Self::ProviderFailure(ProviderFailureReason::StorytellerError(value))
}
}
@@ -1,6 +1,6 @@
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal::handle_generic_bg_removal;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
@@ -41,6 +41,7 @@ use storyteller_client::media_files::get_media_file::get_media_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Manager, State};
use tokens::tokens::media_files::MediaFileToken;
use crate::core::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
#[derive(Deserialize, Debug)]
pub struct EnqueueImageBgRemovalCommand {
@@ -124,6 +125,8 @@ pub async fn enqueue_image_bg_removal_command(
match result {
Err(err) => {
error!("Error removing background: {:?}", err);
notify_frontend_of_errors(&app, &err).await;
// TODO: Derive from err. Make service provider optional.
let event = GenerationEnqueueFailureEvent {
@@ -137,7 +140,7 @@ pub async fn enqueue_image_bg_removal_command(
error!("Failed to emit event: {:?}", err); // Fail open.
}
Err(err.to_tauri_response())
Err(error_to_tauri_response(err))
}
Ok(event) => {
let event = GenerationEnqueueSuccessEvent {
@@ -150,6 +153,8 @@ pub async fn enqueue_image_bg_removal_command(
error!("Failed to emit event: {:?}", err); // Fail open.
}
CreditsBalanceChangedEvent{}.send_infallible(&app);
Ok(EnqueueImageBgRemovalSuccessResponse {}.into())
}
}
@@ -165,7 +170,7 @@ pub async fn handle_request(
storyteller_creds_manager: &StorytellerCredentialManager,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
// TODO(bt,2025-07-07): Other model/provider routing...
@@ -196,3 +201,35 @@ pub async fn handle_request(
Ok(success_event)
}
fn error_to_tauri_response(error: GenerateError) -> CommandErrorResponseWrapper<EnqueueImageBgRemovalErrorType, ()> {
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueImageBgRemovalErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.".to_string();
match error {
GenerateError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueImageBgRemovalErrorType::NoProviderAvailable;
error_message = "No configured provider available for background removal".to_string();
}
GenerateError::BadInput(BadInputReason::RequiredSourceImageNotProvided) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageBgRemovalErrorType::MissingImage;
error_message = "No image provided for background removal".to_string();
}
GenerateError::BadInput(BadInputReason::Base64DecodeError) => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageBgRemovalErrorType::Base64DecodeError;
error_message = "Failed to decode base64 image data".to_string();
}
_ => {}, // Other cases fall through.
}
CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
}
}
@@ -1,71 +0,0 @@
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalErrorType;
use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus};
use errors::AnyhowError;
use serde::Serialize;
use storyteller_client::error::storyteller_error::StorytellerError;
#[derive(Debug)]
pub enum InternalBgRemovalError {
/// No provider is configured for the model type.
NoProviderAvailable,
/// No storyteller credentials are set.
NeedsStorytellerCredentials,
/// No image was provided for background removal.
MissingImage,
/// Could not decode the base64 image data.
Base64DecodeError,
AnyhowError(AnyhowError),
StorytellerError(StorytellerError),
}
impl From<AnyhowError> for InternalBgRemovalError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<StorytellerError> for InternalBgRemovalError {
fn from(value: StorytellerError) -> Self {
Self::StorytellerError(value)
}
}
impl InternalBgRemovalError {
pub fn to_tauri_response<E: Serialize>(&self) -> CommandErrorResponseWrapper<EnqueueImageBgRemovalErrorType, E> {
let mut status = CommandErrorStatus::ServerError;
let mut error_type = EnqueueImageBgRemovalErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.".to_string();
match self {
InternalBgRemovalError::NoProviderAvailable => {
status = CommandErrorStatus::ServerError;
error_type = EnqueueImageBgRemovalErrorType::NoProviderAvailable;
error_message = "No configured provider available for background removal".to_string();
}
InternalBgRemovalError::MissingImage => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageBgRemovalErrorType::MissingImage;
error_message = "No image provided for background removal".to_string();
}
InternalBgRemovalError::Base64DecodeError => {
status = CommandErrorStatus::BadRequest;
error_type = EnqueueImageBgRemovalErrorType::Base64DecodeError;
error_message = "Failed to decode base64 image data".to_string();
}
InternalBgRemovalError::AnyhowError(_) => {}
InternalBgRemovalError::StorytellerError(_) => {}
InternalBgRemovalError::NeedsStorytellerCredentials => {}
}
CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
}
}
}
@@ -1,9 +1,8 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal_artcraft::handle_generic_bg_removal_artcraft;
use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal_fal::handle_generic_bg_removal_fal;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand;
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_artcraft::handle_gpt_image_1_edit_artcraft;
use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit_sora::handle_gpt_image_1_edit_sora;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
@@ -27,7 +26,7 @@ pub async fn handle_generic_bg_removal(
storyteller_creds_manager: &StorytellerCredentialManager,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let priority = provider_priority_store.get_priority()?;
@@ -65,5 +64,5 @@ pub async fn handle_generic_bg_removal(
}
}
Err(InternalBgRemovalError::NoProviderAvailable)
Err(GenerateError::NoProviderAvailable)
}
@@ -1,7 +1,6 @@
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason};
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{EditImageQuality, EditImageSize, EnqueueContextualEditImageCommand};
use crate::core::commands::enqueue::image_edit::errors::InternalContextualEditImageError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
use crate::core::events::generation_events::common::{GenerationAction, GenerationServiceProvider};
@@ -44,12 +43,12 @@ pub async fn handle_generic_bg_removal_artcraft(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
) -> Result<TaskEnqueueSuccess, GenerateError> {
let creds = match storyteller_creds_manager.get_credentials()? {
Some(creds) => creds,
None => {
return Err(InternalBgRemovalError::NeedsStorytellerCredentials);
return Err(GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials));
},
};
@@ -84,7 +83,7 @@ pub async fn handle_generic_bg_removal_artcraft(
}
Err(err) => {
error!("Failed to use Artcraft background removal: {:?}", err);
return Err(InternalBgRemovalError::StorytellerError(err));
return Err(GenerateError::ProviderFailure(ProviderFailureReason::StorytellerError(err)));
}
};
@@ -101,7 +100,7 @@ async fn upload_image_from_base64_bytes(
app_data_root: &AppDataRoot,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet
) -> Result<MediaFileToken, InternalBgRemovalError> {
) -> Result<MediaFileToken, GenerateError> {
let temp_file;
if let Some(base64_bytes) = &request.base64_image {
@@ -110,10 +109,10 @@ async fn upload_image_from_base64_bytes(
.await
.map_err(|err| {
error!("Failed to save base64 image to temp dir: {:?}", err);
InternalBgRemovalError::Base64DecodeError
GenerateError::BadInput(BadInputReason::Base64DecodeError)
})?;
} else {
return Err(InternalBgRemovalError::MissingImage);
return Err(GenerateError::BadInput(BadInputReason::RequiredSourceImageNotProvided));
};
info!("Uploading image media file from temp file: {:?}", temp_file.path());
@@ -129,7 +128,7 @@ async fn upload_image_from_base64_bytes(
}).await
.map_err(|err| {
error!("Failed to upload image media file: {:?}", err);
InternalBgRemovalError::StorytellerError(err)
GenerateError::ProviderFailure(ProviderFailureReason::StorytellerError(err))
})?;
Ok(result.media_file_token)
@@ -1,5 +1,5 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::EnqueueImageBgRemovalCommand;
use crate::core::commands::enqueue::image_bg_removal::errors::InternalBgRemovalError;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
@@ -14,6 +14,6 @@ pub async fn handle_generic_bg_removal_fal(
app_env_configs: &AppEnvConfigs,
fal_creds_manager: &FalCredentialManager,
fal_task_queue: &FalTaskQueue,
) -> Result<TaskEnqueueSuccess, InternalBgRemovalError> {
unimplemented!("TODO: Implement fal background removal handling");
) -> Result<TaskEnqueueSuccess, GenerateError> {
Err(GenerateError::NotYetImplemented("TODO: Implement fal background removal handling".to_string()))
}
@@ -1,3 +1,2 @@
pub mod enqueue_image_bg_removal_command;
pub mod errors;
pub mod generic;

Some files were not shown because too many files have changed in this diff Show More