mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-23 10:09:29 -04:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e17beb58 | ||
|
|
e4f37900e2 | ||
|
|
7b89ad7c2f | ||
|
|
0ee106b3d7 | ||
|
|
76134d106a | ||
|
|
c59e4bcbd4 | ||
|
|
33f659a1ff | ||
|
|
1fbbbc5d7d | ||
|
|
87b4cfb3f2 | ||
|
|
620a4604b0 | ||
|
|
61bbf6d32a | ||
|
|
054eaba609 |
@@ -45,9 +45,6 @@ crates/lib/sqlite_queries/target/
|
||||
# Javascript build directories
|
||||
node_modules/
|
||||
|
||||
# Frontend build outputs
|
||||
frontend/apps/editor3d/bundle-report.html
|
||||
|
||||
# Vim noise
|
||||
*.swo
|
||||
*.swp
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE tasks\n SET is_dismissed_by_user = 1\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "13d435d8f681b73e4080748dcef0579e246e41049af67e1bd79ffc06aa6ba974"
|
||||
}
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n on_complete_primary_media_file_token,\n on_complete_primary_media_file_class,\n on_complete_batch_token,\n on_complete_primary_media_file_cdn_url,\n on_complete_primary_media_file_thumbnail_url_template,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "task_status",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "task_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model_type",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "provider",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "provider_job_id",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "frontend_caller",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "frontend_subscriber_id",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "frontend_subscriber_payload",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_primary_media_file_token",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_primary_media_file_class",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_batch_token",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_primary_media_file_cdn_url",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "on_complete_primary_media_file_thumbnail_url_template",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"ordinal": 14,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"ordinal": 15,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "completed_at: DateTime<Utc>",
|
||||
"ordinal": 16,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "47f94c127abc20683b25c257aed3c2190b9bd29eb829d61392f08e10fd986767"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO analytics_app_active_users\nSET\n app_namespace = ?,\n user_token = ?,\n app_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n first_active_at = NOW(),\n last_active_at = NOW()\nON DUPLICATE KEY UPDATE\n app_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n last_active_at = NOW()\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 8
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4d4e7074bb73d1480d6f1a07f3698c9ca011ac445302b7f462f265fe5f296a8c"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE tasks\n SET\n task_status = ?,\n on_complete_batch_token = ?,\n on_complete_primary_media_file_token = ?,\n on_complete_primary_media_file_class = ?,\n on_complete_primary_media_file_cdn_url = ?,\n on_complete_primary_media_file_thumbnail_url_template = ?\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "50a2449b87abed7df3302a74ecd0e8ab87940077a247636680cb09b2fe488321"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO analytics_app_active_users\nSET\n app_namespace = ?,\n user_token = ?,\n app_version = ?,\n os_platform = ?,\n os_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n first_active_at = NOW(),\n last_active_at = NOW()\nON DUPLICATE KEY UPDATE\n app_version = ?,\n os_platform = ?,\n os_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n last_active_at = NOW()\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 12
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b48490837d568002305551353704c8e8a4fc78459e5a9aa754277b989107bc5c"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO analytics_app_sessions\nSET\n session_token = ?,\n app_namespace = ?,\n user_token = ?,\n app_version = ?,\n os_platform = ?,\n os_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n first_active_at = NOW(),\n last_active_at = NOW()\nON DUPLICATE KEY UPDATE\n app_namespace = ?,\n user_token = ?,\n app_version = ?,\n os_platform = ?,\n os_version = ?,\n session_duration_seconds = ?,\n ip_address = ?,\n measurement_count = measurement_count + 1,\n last_active_at = NOW()\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 15
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ff39576a5ec6a23c67f6f61e7eba8392e70b05c42b540bea260a9a4cdc478a5b"
|
||||
}
|
||||
Generated
+225
-219
File diff suppressed because it is too large
Load Diff
+5
-18
@@ -6,8 +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/artcraft_api_defs", # TODO: api_clients/{artcraft}/{api_defs,client}
|
||||
"crates/api_clients/artcraft/storyteller_client",
|
||||
"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",
|
||||
@@ -16,6 +15,7 @@ members = [
|
||||
"crates/api_clients/openai_sora_client",
|
||||
"crates/api_clients/openart_client",
|
||||
"crates/api_clients/runwayml_client",
|
||||
"crates/api_clients/storyteller_client",
|
||||
"crates/api_clients/thumbnail_generator",
|
||||
|
||||
# CLI tools for operations, etc.
|
||||
@@ -181,7 +181,7 @@ 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/artcraft_api_defs" }
|
||||
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" }
|
||||
@@ -205,7 +205,7 @@ 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/artcraft/storyteller_client" }
|
||||
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" }
|
||||
@@ -237,7 +237,7 @@ log = "0.4.27"
|
||||
once_cell = "1.21.3"
|
||||
rand = "0.9.1"
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
|
||||
reqwest = { version = "0.12.22", features = ["json", "cookies"] }
|
||||
reqwest-websocket = { version = "0.5.0" }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_derive = "1.0.219"
|
||||
@@ -255,16 +255,3 @@ thiserror = "2.0.12"
|
||||
tokio = { version = "1.45.1", features = ["macros"] }
|
||||
toml = "0.8.22"
|
||||
url = { version = "2.5.4", features = ["serde"] }
|
||||
|
||||
# NB: wreq gets past fingerprinting, but we have to deal with platform-specific build issues.
|
||||
#
|
||||
# Mac:
|
||||
# brew install --formula cmake pkg-config llvm
|
||||
# Windows:
|
||||
# choco install cmake -y
|
||||
# choco install strawberryperl -y
|
||||
# choco install pkgconfiglite -y
|
||||
# choco install llvm -y
|
||||
# choco install nasm -y
|
||||
wreq = { version = "6.0.0-rc.20", features = ["json", "multipart", "cookies"] }
|
||||
wreq-util = "3.0.0-rc.5"
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
select
|
||||
u.display_name,
|
||||
count(*) as sessions,
|
||||
a.session_duration_seconds / 60 / 60 as hours,
|
||||
a.measurement_count,
|
||||
u.email_address,
|
||||
a.os_platform,
|
||||
a.os_version,
|
||||
u.created_at,
|
||||
u.maybe_source,
|
||||
a.last_active_at
|
||||
from analytics_app_sessions as a
|
||||
left outer join users as u
|
||||
on a.user_token = u.token
|
||||
group by a.user_token
|
||||
order by hours desc;
|
||||
@@ -9,14 +9,6 @@
|
||||
-- tasks_v1.sqlite - initial version
|
||||
-- tasks_v2.sqlite - added model_type (nullable)
|
||||
-- tasks_v3.sqlite - added frontend_caller, comments
|
||||
-- tasks_v4.sqlite - added is_dismissed_by_user
|
||||
-- tasks_v5.sqlite - added five fields:
|
||||
-- on_complete_batch_token,
|
||||
-- on_complete_primary_media_file_token,
|
||||
-- on_complete_primary_media_file_class,
|
||||
-- on_complete_primary_media_file_cdn_url,
|
||||
-- on_complete_primary_media_file_thumbnail_url_template,
|
||||
--
|
||||
|
||||
CREATE TABLE tasks (
|
||||
-- Task auto-incrementing primary key.
|
||||
@@ -58,32 +50,6 @@ CREATE TABLE tasks (
|
||||
-- will be re-emitted back to the frontend.
|
||||
frontend_subscriber_payload TEXT,
|
||||
|
||||
-- Whether the user has dismissed the task from view.
|
||||
is_dismissed_by_user INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the batch token (if any).
|
||||
-- This will point to a collection of multiple files.
|
||||
on_complete_batch_token TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the media token of the "primary" media file.
|
||||
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
|
||||
on_complete_primary_media_file_token TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the file type of the primary media file.
|
||||
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
|
||||
on_complete_primary_media_file_class TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- The URL to the media file.
|
||||
on_complete_primary_media_file_cdn_url TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- The URL template for the thumbnail of the primary media file.
|
||||
on_complete_primary_media_file_thumbnail_url_template TEXT,
|
||||
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
completed_at INTEGER DEFAULT NULL
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Track user activity over time
|
||||
-- User clients ping the endpoint that writes this data.
|
||||
CREATE TABLE analytics_app_active_users (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
@@ -19,14 +17,6 @@ CREATE TABLE analytics_app_active_users (
|
||||
-- The "user agent" or app version string of the client.
|
||||
app_version VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The OS platform for the client.
|
||||
os_platform VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The OS platform version for the client.
|
||||
os_version VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- How long the current user's session has been open
|
||||
session_duration_seconds INT(10) UNSIGNED DEFAULT NULL,
|
||||
@@ -51,9 +41,6 @@ CREATE TABLE analytics_app_active_users (
|
||||
-- ========== INDICES ==========
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (app_namespace, user_token), -- For now
|
||||
INDEX idx_os_platform (os_platform),
|
||||
INDEX idx_os_version (os_version),
|
||||
INDEX idx_app_version (app_version),
|
||||
INDEX idx_last_active_at (last_active_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
drop table analytics_app_sessions;
|
||||
@@ -1,65 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Track user session durations
|
||||
-- User clients ping the endpoint that writes this data.
|
||||
CREATE TABLE analytics_app_sessions (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Generated client-side
|
||||
-- Wide enough for a UUID, but typically one of our internal tokens.
|
||||
session_token VARCHAR(36) NOT NULL,
|
||||
|
||||
-- If we host multiple sites with distinct categories, this will enable us to
|
||||
-- segregate analytics.
|
||||
app_namespace VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Who sent the event.
|
||||
user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The "user agent" or app version string of the client.
|
||||
app_version VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The OS platform for the client.
|
||||
os_platform VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The OS platform version for the client.
|
||||
os_version VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- How long the current user's session has been open
|
||||
session_duration_seconds INT(10) UNSIGNED DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The user's last known IP address.
|
||||
ip_address VARCHAR(40) NOT NULL,
|
||||
|
||||
-- Incrementing count of events.
|
||||
-- NB: This is kind of an "hours spent" measure, but will lose information if we ever
|
||||
-- change the ping cadence (eg. 1 minute pings --> 5 minute pings).
|
||||
measurement_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
|
||||
-- ========== RECORD TIMESTAMPS ==========
|
||||
|
||||
-- Written at create only.
|
||||
first_active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Updated on every ping.
|
||||
last_active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- ========== INDICES ==========
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (session_token),
|
||||
INDEX idx_app_namespace (app_namespace),
|
||||
INDEX idx_user_token (user_token),
|
||||
INDEX idx_os_platform (os_platform),
|
||||
INDEX idx_os_version (os_version),
|
||||
INDEX idx_app_version (app_version),
|
||||
INDEX idx_last_active_at (last_active_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
@@ -1,65 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Track user session durations
|
||||
-- User clients ping the endpoint that writes this data.
|
||||
CREATE TABLE analytics_app_sessions (
|
||||
-- Not used for anything except replication.
|
||||
id BIGINT(20) NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- Generated client-side
|
||||
-- Wide enough for a UUID, but typically one of our internal tokens.
|
||||
session_token VARCHAR(36) NOT NULL,
|
||||
|
||||
-- If we host multiple sites with distinct categories, this will enable us to
|
||||
-- segregate analytics.
|
||||
app_namespace VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Who sent the event.
|
||||
user_token VARCHAR(32) NOT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The "user agent" or app version string of the client.
|
||||
app_version VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The OS platform for the client.
|
||||
os_platform VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The OS platform version for the client.
|
||||
os_version VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- How long the current user's session has been open
|
||||
session_duration_seconds INT(10) UNSIGNED DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The user's last known IP address.
|
||||
ip_address VARCHAR(40) NOT NULL,
|
||||
|
||||
-- Incrementing count of events.
|
||||
-- NB: This is kind of an "hours spent" measure, but will lose information if we ever
|
||||
-- change the ping cadence (eg. 1 minute pings --> 5 minute pings).
|
||||
measurement_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
|
||||
-- ========== RECORD TIMESTAMPS ==========
|
||||
|
||||
-- Written at create only.
|
||||
first_active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Updated on every ping.
|
||||
last_active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- ========== INDICES ==========
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (session_token),
|
||||
INDEX idx_app_namespace (app_namespace),
|
||||
INDEX idx_user_token (user_token),
|
||||
INDEX idx_os_platform (os_platform),
|
||||
INDEX idx_os_version (os_version),
|
||||
INDEX idx_app_version (app_version),
|
||||
INDEX idx_last_active_at (last_active_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
-11
@@ -17,14 +17,6 @@ CREATE TABLE analytics_app_active_users (
|
||||
-- The "user agent" or app version string of the client.
|
||||
app_version VARCHAR(255) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The OS platform for the client.
|
||||
os_platform VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- The OS platform version for the client.
|
||||
os_version VARCHAR(64) DEFAULT NULL,
|
||||
|
||||
-- Updated on every write.
|
||||
-- How long the current user's session has been open
|
||||
session_duration_seconds INT(10) UNSIGNED DEFAULT NULL,
|
||||
@@ -49,9 +41,6 @@ CREATE TABLE analytics_app_active_users (
|
||||
-- ========== INDICES ==========
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (app_namespace, user_token), -- For now
|
||||
INDEX idx_app_version (app_version),
|
||||
INDEX idx_os_platform (os_platform),
|
||||
INDEX idx_os_version (os_version),
|
||||
INDEX idx_last_active_at (last_active_at)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct SimpleGenericJsonSuccess {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct DeleteMediaFileRequest {
|
||||
pub set_delete: bool,
|
||||
|
||||
/// NB: this is only to disambiguate when a user is both a mod and an author.
|
||||
pub as_mod: Option<bool>,
|
||||
}
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct DeleteMediaFilePathInfo {
|
||||
pub token: MediaFileToken,
|
||||
}
|
||||
|
||||
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
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::generate::video::generate_veo_3_fast_image_to_video::{GenerateVeo3FastImageToVideoRequest, GenerateVeo3FastImageToVideoResponse, GENERATE_VEO_3_FAST_IMAGE_TO_VIDEO_URL_PATH};
|
||||
|
||||
pub async fn generate_veo_3_fast_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateVeo3FastImageToVideoRequest,
|
||||
) -> Result<GenerateVeo3FastImageToVideoResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_VEO_3_FAST_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
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::generate::video::generate_veo_3_image_to_video::{GenerateVeo3ImageToVideoRequest, GenerateVeo3ImageToVideoResponse, GENERATE_VEO_3_IMAGE_TO_VIDEO_URL_PATH};
|
||||
|
||||
pub async fn generate_veo_3_image_to_video(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateVeo3ImageToVideoRequest,
|
||||
) -> Result<GenerateVeo3ImageToVideoResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_VEO_3_IMAGE_TO_VIDEO_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_delete_request::basic_json_delete_request;
|
||||
use artcraft_api_defs::common::responses::simple_generic_json_success::SimpleGenericJsonSuccess;
|
||||
use artcraft_api_defs::media_file::delete_media_file::DeleteMediaFileRequest;
|
||||
use artcraft_api_defs::subscriptions::get_session_subscription::GetSessionSubscriptionResponse;
|
||||
use enums::common::payments_namespace::PaymentsNamespace;
|
||||
use log::debug;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub async fn delete_media_file(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
media_file_token: &MediaFileToken,
|
||||
) -> Result<SimpleGenericJsonSuccess, StorytellerError> {
|
||||
let url_path = get_url_path(media_file_token);
|
||||
|
||||
debug!("Requesting {:?}", &url_path);
|
||||
|
||||
// NB: We probably don't need to expose these options from FakeYou anymore.
|
||||
let request = DeleteMediaFileRequest {
|
||||
set_delete: true,
|
||||
as_mod: None,
|
||||
};
|
||||
|
||||
Ok(basic_json_delete_request(
|
||||
api_host,
|
||||
&url_path,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
|
||||
fn get_url_path(token: &MediaFileToken) -> String {
|
||||
format!("/v1/media_files/file/{}", token.as_str())
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//! An API client for Artcraft / Storyteller / FakeYou
|
||||
|
||||
pub mod credentials;
|
||||
pub mod endpoints;
|
||||
pub mod error;
|
||||
pub mod recipes;
|
||||
pub mod utils;
|
||||
@@ -1,71 +0,0 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
|
||||
use crate::utils::filter_bad_response::filter_bad_response;
|
||||
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH};
|
||||
use log::{debug, info};
|
||||
use reqwest::Client;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub async fn basic_json_delete_request<Req: Serialize, Res: DeserializeOwned>(
|
||||
api_host: &ApiHost,
|
||||
route_path: &str,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: Req,
|
||||
) -> Result<Res, StorytellerError> {
|
||||
|
||||
let url = get_route(api_host, route_path);
|
||||
|
||||
info!("DELETE {:?}", &url);
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let mut request_builder = client.delete(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", APPLICATION_JSON)
|
||||
.header("Content-Type", APPLICATION_JSON);
|
||||
|
||||
if let Some(creds) = maybe_creds {
|
||||
if let Some(header) = &creds.maybe_as_cookie_header() {
|
||||
request_builder = request_builder.header("Cookie", header);
|
||||
}
|
||||
}
|
||||
|
||||
let request_body = serde_json::to_string(&request)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
debug!("Request body: {:?}", request_body);
|
||||
|
||||
let response = request_builder
|
||||
.body(request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response = filter_bad_response(response)
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
let response_body = &response.text()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
debug!("Response body: {:?}", response_body);
|
||||
|
||||
let response = serde_json::from_str(&response_body)
|
||||
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_route(api_host: &ApiHost, route_path: &str) -> String {
|
||||
let api_hostname = api_host.to_api_hostname_and_scheme();
|
||||
format!("{}{}", api_hostname, route_path)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
pub (crate) mod basic_json_delete_request;
|
||||
pub (crate) mod basic_json_get_request;
|
||||
pub (crate) mod basic_json_post_request;
|
||||
pub (crate) mod basic_query_string_post_request;
|
||||
pub (crate) mod filter_bad_response;
|
||||
pub (crate) mod http_get_anonymous;
|
||||
pub mod api_host;
|
||||
pub mod constants;
|
||||
pub mod status_codes;
|
||||
+1
-12
@@ -1,26 +1,15 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::app_session::AppSessionToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const LOG_ACTIVE_USER_PATH: &str = "/v1/analytics/active_user";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct LogAppActiveUserRequest {
|
||||
/// Clientside-generated session token.
|
||||
/// If present, it'll be validated against an expected format.
|
||||
pub maybe_app_session_token: Option<AppSessionToken>,
|
||||
|
||||
/// An override for the application platform/OS (windows, mac, linux).
|
||||
pub maybe_os_platform: Option<String>,
|
||||
|
||||
/// An override for the version of the OS (e.g. 10.15.7, 11, 22.04).
|
||||
pub maybe_os_version: Option<String>,
|
||||
|
||||
/// An override for the application name.
|
||||
/// If set together with `maybe_app_version`, the two will be
|
||||
/// concatenated as `{maybe_app_name}/{maybe_app_version}`.
|
||||
pub maybe_app_name: Option<String>,
|
||||
|
||||
|
||||
/// An override for the application version.
|
||||
/// If set together with `maybe_app_name`, the two will be
|
||||
/// concatenated as `{maybe_app_name}/{maybe_app_version}`.
|
||||
-1
@@ -3,5 +3,4 @@ pub mod job_details;
|
||||
pub mod media_file_cover_image_details;
|
||||
pub mod media_links;
|
||||
pub mod simple_entity_stats;
|
||||
pub mod simple_generic_json_success;
|
||||
pub mod user_details_light;
|
||||
-1
@@ -1,2 +1 @@
|
||||
pub mod delete_media_file;
|
||||
pub mod list_batch_generated_media_files;
|
||||
@@ -24,12 +24,27 @@ errors.workspace = true
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
#reqwest-websocket.workspace = true
|
||||
#reqwest.workspace = true
|
||||
#reqwest-impersonate = { version = "0", features = ["websocket"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
#tokio.workspace = true
|
||||
url.workspace = true
|
||||
wreq-util.workspace = true
|
||||
wreq.workspace = true
|
||||
|
||||
# NB: wreq gets past fingerprinting, but we have to deal with platform-specific build issues.
|
||||
#
|
||||
# Mac:
|
||||
# brew install --formula cmake pkg-config llvm
|
||||
# Windows:
|
||||
# choco install cmake -y
|
||||
# choco install strawberryperl -y
|
||||
# choco install pkgconfiglite -y
|
||||
# choco install llvm -y
|
||||
# choco install nasm -y
|
||||
wreq = { version = "5", features = ["json"] }
|
||||
wreq-util = "2"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# NB: We need to parse HTML to get out the websocket token
|
||||
dom_query = "0.20.2"
|
||||
|
||||
@@ -28,19 +28,14 @@ cookie.workspace = true
|
||||
log.workspace = true
|
||||
once_cell.workspace = true
|
||||
rand.workspace = true
|
||||
#reqwest = { workspace = true, features = ["multipart", "gzip"] }
|
||||
reqwest = { workspace = true, features = ["multipart", "gzip"] }
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha3.workspace = true
|
||||
thiserror.workspace = true
|
||||
#tokio.workspace = true
|
||||
tokio.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
wreq-util.workspace = true
|
||||
wreq.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
testing = { path = "../../lib/testing" }
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub (crate) mod user_agent;
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
// TODO: enum UserAgent { Mac, Linux, Windows } which maps to these.
|
||||
|
||||
/// This user agent is tied to the sentinel generation.
|
||||
/// If we need to change it, we may need to change sentinel generation too.
|
||||
pub (crate) const USER_AGENT : &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
|
||||
|
||||
|
||||
// NB: from sentinel token request -
|
||||
// /// This user agent is tied to the sentinel generation. If we need to change it, we may need to change sentinel generation too.
|
||||
// const USER_AGENT : &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
|
||||
|
||||
// NB: from image gen request -
|
||||
// /// This user agent is tied to the sentinel generation. If we need to change it, we may need to change sentinel generation too.
|
||||
// //const USER_AGENT : &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
|
||||
|
||||
// Previous job status user agent
|
||||
// const USER_AGENT : &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
|
||||
|
||||
// NB: image gen status -
|
||||
// /// This user agent is tied to the sentinel generation. If we need to change it, we may need to change sentinel generation too.
|
||||
// pub (crate) const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
|
||||
@@ -0,0 +1,60 @@
|
||||
use std::path::Path;
|
||||
use reqwest::RequestBuilder;
|
||||
use serde_derive::Deserialize;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
// NB: It appears that the sentinel may require a matching user agent.
|
||||
pub const USER_AGENT : &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub struct SoraCredentials {
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub bearer_token: String,
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub cookie: String,
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub sentinel: Option<String>,
|
||||
}
|
||||
|
||||
impl SoraCredentials {
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub fn authorization_header_value(&self) -> String {
|
||||
match self.bearer_token.get(0..6) {
|
||||
Some("bearer") | Some("Bearer") => self.bearer_token.clone(),
|
||||
_ => "Bearer ".to_owned() + &self.bearer_token,
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub fn from_env() -> AnyhowResult<Self> {
|
||||
let bearer = std::env::var("SORA_BEARER_TOKEN")?;
|
||||
let cookie = std::env::var("SORA_COOKIE")?;
|
||||
let sentinel = std::env::var("SORA_SENTINEL").ok();
|
||||
|
||||
Ok(Self { bearer_token: bearer, cookie, sentinel })
|
||||
}
|
||||
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub fn from_toml_file<P: AsRef<Path>>(filename: P) -> AnyhowResult<Self> {
|
||||
let contents = std::fs::read_to_string(filename)?;
|
||||
let credentials = toml::from_str(&contents)?;
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub fn add_credential_headers_to_request(&self, request: RequestBuilder) -> RequestBuilder {
|
||||
let bearer_header = self.authorization_header_value();
|
||||
|
||||
let mut request = request
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Cookie", &self.cookie)
|
||||
.header("Authorization", bearer_header);
|
||||
|
||||
if let Some(sentinel) = &self.sentinel {
|
||||
request = request.header("OpenAI-Sentinel-Token", sentinel);
|
||||
}
|
||||
|
||||
request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
|
||||
pub enum CredentialMigration {
|
||||
Legacy(SoraCredentials),
|
||||
New(SoraCredentialSet),
|
||||
}
|
||||
|
||||
pub enum CredentialMigrationRef<'a> {
|
||||
Legacy(&'a SoraCredentials),
|
||||
New(&'a SoraCredentialSet),
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod credential_migration;
|
||||
pub mod sora_cookies;
|
||||
pub mod sora_credential_builder;
|
||||
pub mod sora_credential_set;
|
||||
pub mod sora_jwt_bearer_token;
|
||||
pub mod sora_sentinel;
|
||||
@@ -1,76 +0,0 @@
|
||||
use crate::creds::sora_cookies::SoraCookies;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use crate::creds::sora_sentinel::SoraSentinel;
|
||||
use crate::error::sora_client_error::SoraClientError;
|
||||
use crate::error::sora_error::SoraError;
|
||||
|
||||
pub struct SoraCredentialBuilder {
|
||||
cookies: Option<String>,
|
||||
jwt_bearer_token: Option<String>,
|
||||
sora_sentinel: Option<String>,
|
||||
}
|
||||
|
||||
impl SoraCredentialBuilder {
|
||||
pub fn new() -> Self {
|
||||
SoraCredentialBuilder {
|
||||
cookies: None,
|
||||
jwt_bearer_token: None,
|
||||
sora_sentinel: None,
|
||||
}
|
||||
}
|
||||
|
||||
// Consuming methods
|
||||
|
||||
pub fn with_cookies(mut self, cookies: &str) -> Self {
|
||||
self.cookies = Some(cookies.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_jwt_bearer_token(mut self, token: &str) -> Self {
|
||||
self.jwt_bearer_token = Some(token.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_sora_sentinel(mut self, sentinel: &str) -> Self {
|
||||
self.sora_sentinel = Some(sentinel.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
// Mutable methods
|
||||
|
||||
pub fn set_cookies(&mut self, cookies: &str) {
|
||||
self.cookies = Some(cookies.to_string());
|
||||
}
|
||||
|
||||
pub fn set_jwt_bearer_token(&mut self, token: &str) {
|
||||
self.jwt_bearer_token = Some(token.to_string());
|
||||
}
|
||||
|
||||
pub fn set_sora_sentinel(&mut self, sentinel: &str) {
|
||||
self.sora_sentinel = Some(sentinel.to_string());
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<SoraCredentialSet, SoraError> {
|
||||
let cookies = match self.cookies {
|
||||
Some(c) => SoraCookies::new(c),
|
||||
None => return Err(SoraClientError::SoraCredentialBuilderError("no cookies provided").into()),
|
||||
};
|
||||
|
||||
let jwt_bearer_token = match self.jwt_bearer_token {
|
||||
Some(t) => Some(SoraJwtBearerToken::new(t)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let sora_sentinel = match self.sora_sentinel {
|
||||
Some(s) => Some(SoraSentinel::new(s)),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(SoraCredentialSet {
|
||||
cookies,
|
||||
jwt_bearer_token,
|
||||
sora_sentinel,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::creds::sora_cookies::SoraCookies;
|
||||
use crate::creds::sora_credential_builder::SoraCredentialBuilder;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use crate::creds::sora_sentinel::SoraSentinel;
|
||||
|
||||
@@ -36,16 +38,38 @@ impl SoraCredentialSet {
|
||||
SoraCookies::new(cookies.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_builder(&self) -> SoraCredentialBuilder {
|
||||
let mut builder = SoraCredentialBuilder::new()
|
||||
.with_cookies(self.cookies.as_str());
|
||||
if let Some(bearer) = &self.jwt_bearer_token {
|
||||
builder = builder.with_jwt_bearer_token(bearer.as_str());
|
||||
|
||||
// TODO(bt,2025-04-23): Just here for migration
|
||||
pub fn from_legacy_credentials(credentials: &SoraCredentials) -> AnyhowResult<Self> {
|
||||
let cookies = SoraCookies::new(credentials.cookie.clone());
|
||||
let jwt_bearer_token = SoraJwtBearerToken::new(credentials.bearer_token.clone())?;
|
||||
let mut sora_sentinel = None;
|
||||
|
||||
if let Some(sentinel) = &credentials.sentinel {
|
||||
sora_sentinel = Some(SoraSentinel::new(sentinel.clone()));
|
||||
}
|
||||
if let Some(sentinel) = &self.sora_sentinel {
|
||||
builder = builder.with_sora_sentinel(sentinel.as_str());
|
||||
}
|
||||
builder
|
||||
|
||||
Ok(SoraCredentialSet {
|
||||
cookies,
|
||||
jwt_bearer_token: Some(jwt_bearer_token),
|
||||
sora_sentinel,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_legacy_credentials(&self) -> AnyhowResult<SoraCredentials> {
|
||||
let bearer_token= self.jwt_bearer_token
|
||||
.as_ref()
|
||||
.map(|token| token.token_str().to_string())
|
||||
.ok_or_else(|| anyhow!("There is no bearer token to convert to a required legacy bearer token"))?;
|
||||
|
||||
let sentinel = self.sora_sentinel
|
||||
.as_ref()
|
||||
.map(|sentinel| sentinel.get_sentinel().to_string());
|
||||
|
||||
Ok(SoraCredentials {
|
||||
cookie: self.cookies.as_str().to_string(),
|
||||
bearer_token,
|
||||
sentinel,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::utils_internal::lightweight_sora_jwt_parse::{lightweight_sora_jwt_parse, SoraJwtClaims};
|
||||
use crate::utils::lightweight_sora_jwt_parse::{lightweight_sora_jwt_parse, SoraJwtClaims};
|
||||
use errors::AnyhowResult;
|
||||
|
||||
/// Sora bearer tokens are JWT tokens that can be minted with a valid cookie/session.
|
||||
#[derive(Clone)]
|
||||
@@ -9,7 +9,7 @@ pub struct SoraJwtBearerToken {
|
||||
}
|
||||
|
||||
impl SoraJwtBearerToken {
|
||||
pub fn new(token: String) -> Result<Self, SoraError> {
|
||||
pub fn new(token: String) -> AnyhowResult<Self> {
|
||||
let jwt_claims = lightweight_sora_jwt_parse(&token)?;
|
||||
Ok(SoraJwtBearerToken {
|
||||
token,
|
||||
@@ -20,10 +20,6 @@ impl SoraJwtBearerToken {
|
||||
pub fn token_str(&self) -> &str {
|
||||
&self.token
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.token
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.token.as_bytes()
|
||||
|
||||
@@ -17,10 +17,6 @@ impl SoraSentinel {
|
||||
&self.sentinel
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.sentinel
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.sentinel.as_bytes()
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod sora_client_error;
|
||||
pub mod sora_error;
|
||||
pub mod sora_generic_api_error;
|
||||
pub mod sora_specific_api_error;
|
||||
@@ -1,53 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SoraClientError {
|
||||
/// An error reading the file for upload.
|
||||
FileForUploadReadError(std::io::Error),
|
||||
|
||||
/// The file path provided for upload is invalid.
|
||||
FileForUploadHasInvalidPath,
|
||||
|
||||
/// Something is wrong with the JWT bearer token.
|
||||
/// This error originates on our end as we try to parse the JWT.
|
||||
LocalJwtClaimsParseError(String),
|
||||
|
||||
/// There was an error constructing the form-multipart request.
|
||||
MultipartFormError(wreq::Error),
|
||||
|
||||
/// We haven't received a bearer token yet
|
||||
/// This is our own internal application state error, not something Sora returns.
|
||||
/// We know our client can't make the request, so we preemptively fail it.
|
||||
NoBearerTokenForRequest,
|
||||
|
||||
/// A sentinel token is not present in the client, which is required for some requests.
|
||||
NoSentinelTokenForRequest,
|
||||
|
||||
/// Issue with using the SoraCredentialBuilder.
|
||||
SoraCredentialBuilderError(&'static str),
|
||||
|
||||
/// Error parsing a request URL.
|
||||
UrlParseError(url::ParseError),
|
||||
|
||||
/// An error was encountered in building the Wreq client
|
||||
WreqClientError(wreq::Error),
|
||||
}
|
||||
|
||||
impl Error for SoraClientError {}
|
||||
|
||||
impl Display for SoraClientError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::FileForUploadReadError(err) => write!(f, "Error reading file for upload: {}", err),
|
||||
Self::FileForUploadHasInvalidPath => write!(f, "The file path provided for upload is invalid."),
|
||||
Self::LocalJwtClaimsParseError(msg) => write!(f, "Local JWT claims parse error: {}", msg),
|
||||
Self::MultipartFormError(err) => write!(f, "Multipart form error: {}", err),
|
||||
Self::NoBearerTokenForRequest => write!(f, "No bearer token available. The client needs a bearer token to make the request."),
|
||||
Self::NoSentinelTokenForRequest => write!(f, "No sentinel token available. The client needs a sentinel token to make the request."),
|
||||
Self::SoraCredentialBuilderError(msg) => write!(f, "Sora Credential Builder error: {}", msg),
|
||||
Self::UrlParseError(err) => write!(f, "URL parse error: {}", err),
|
||||
Self::WreqClientError(err) => write!(f, "Wreq client error (during client creation): {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use crate::error::sora_client_error::SoraClientError;
|
||||
use crate::error::sora_generic_api_error::SoraGenericApiError;
|
||||
use crate::error::sora_specific_api_error::SoraSpecificApiError;
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SoraError {
|
||||
Client(SoraClientError),
|
||||
ApiSpecific(SoraSpecificApiError),
|
||||
ApiGeneric(SoraGenericApiError),
|
||||
}
|
||||
|
||||
impl SoraError {
|
||||
pub fn is_sora_having_downtime_issues(&self) -> bool {
|
||||
match self {
|
||||
Self::ApiGeneric(SoraGenericApiError::Http502ErrorBadGateway(_)) => true,
|
||||
Self::ApiGeneric(SoraGenericApiError::CloudflareError(CloudflareError::BadGateway502)) => true,
|
||||
Self::ApiGeneric(SoraGenericApiError::CloudflareError(CloudflareError::GatewayTimeout504)) => true,
|
||||
Self::ApiGeneric(SoraGenericApiError::CloudflareError(CloudflareError::TimeoutOccurred524)) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for SoraError {}
|
||||
|
||||
impl std::fmt::Display for SoraError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Client(e) => write!(f, "SoraClientError: {:?}", e),
|
||||
Self::ApiSpecific(e) => write!(f, "SoraSpecificApiError: {:?}", e),
|
||||
Self::ApiGeneric(e) => write!(f, "SoraGenericApiError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SoraClientError> for SoraError {
|
||||
fn from(error: SoraClientError) -> Self {
|
||||
Self::Client(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SoraSpecificApiError> for SoraError {
|
||||
fn from(error: SoraSpecificApiError) -> Self {
|
||||
Self::ApiSpecific(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SoraGenericApiError> for SoraError {
|
||||
fn from(error: SoraGenericApiError) -> Self {
|
||||
Self::ApiGeneric(error)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use wreq::StatusCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SoraGenericApiError {
|
||||
/// Specific Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
/// HTTP 502 Bad Gateway
|
||||
/// Sora is having issues.
|
||||
/// Typically served from Cloudflare
|
||||
/// We preserve the message for debugging.
|
||||
Http502ErrorBadGateway(String),
|
||||
|
||||
/// serde_json::Error, likely from JSON deserialization schema mismatch.
|
||||
/// Includes the original body.
|
||||
SerdeResponseParseErrorWithBody(serde_json::Error, String),
|
||||
|
||||
/// serde_json::Error, likely from JSON deserialization schema mismatch.
|
||||
/// Includes the original body.
|
||||
/// Specifically, this is for non-200 (error) responses.
|
||||
SerdeParseErrorWithBodyOnNon200(serde_json::Error, String),
|
||||
|
||||
/// Another error occurred.
|
||||
/// TODO: Try to get rid of this.
|
||||
UncategorizedBadResponse(String),
|
||||
|
||||
/// An uncategorized bad HTTP response from Sora.
|
||||
UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
|
||||
/// An uncaught error from the API client.
|
||||
WreqError(wreq::Error),
|
||||
}
|
||||
|
||||
impl Error for SoraGenericApiError {}
|
||||
|
||||
impl Display for SoraGenericApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CloudflareError(err) => write!(f, "Cloudflare error: {}", err),
|
||||
Self::Http502ErrorBadGateway(msg) => write!(f, "HTTP 502 Bad Gateway: {}", msg),
|
||||
Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body),
|
||||
Self::SerdeParseErrorWithBodyOnNon200(err, body) => write!(f, "Failed to parse non-200 response body: {:?}. Body: {}", err, body),
|
||||
Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body),
|
||||
Self::UncategorizedBadResponse(msg) => write!(f, "Uncategorized bad response status: {}", msg),
|
||||
Self::WreqError(err) => write!(f, "Wreq client error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SoraSpecificApiError {
|
||||
/// Failed to generate a bearer token despite the `200 OK` response.
|
||||
/// This has been generated by sending old/expired session cookies or empty cookies.
|
||||
FailedToGenerateBearerToken,
|
||||
|
||||
/// The endpoint told us we used an invalid JWT.
|
||||
InvalidJwt,
|
||||
|
||||
/// Sora has responded with "sentinel block".
|
||||
/// This usually means we need to refresh our sentinel token.
|
||||
SentinelBlockError,
|
||||
|
||||
/// Sora requires users to set up an account username before they can generate images.
|
||||
/// This requires the user to set up that username.
|
||||
SoraUsernameNotYetCreated,
|
||||
|
||||
/// Sora has responded with "token expired".
|
||||
/// This (probably?) means we need to refresh our JWT bearer token.
|
||||
TokenExpiredError,
|
||||
|
||||
/// We're sending too many tasks to Sora.
|
||||
/// Sora is telling us to cool it.
|
||||
TooManyConcurrentTasks,
|
||||
|
||||
// TODO: Try to make concrete error types for these distinct cases.
|
||||
/// Unauthorized, cookie and/or bearer token expired. We'll need to ask for a refreshed login.
|
||||
///
|
||||
/// Example message, e.g. from the upload endpoint:
|
||||
/// {
|
||||
/// "error": {
|
||||
/// "message": "Your authentication token has expired. Please try signing in again.",
|
||||
/// "type": "invalid_request_error",
|
||||
/// "param": null,
|
||||
/// "code": "token_expired"
|
||||
/// }
|
||||
/// }
|
||||
UnauthorizedCookieOrBearerExpired,
|
||||
}
|
||||
|
||||
impl Error for SoraSpecificApiError {}
|
||||
|
||||
impl Display for SoraSpecificApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::FailedToGenerateBearerToken => write!(f, "Failed to generate bearer token. Probably invalid or expired session cookies."),
|
||||
Self::InvalidJwt => write!(f, "Invalid JWT token. Please refresh the token."),
|
||||
Self::SentinelBlockError => write!(f, "Request blocked by Sentinel Token Block. Please refresh the sentinel token."),
|
||||
Self::SoraUsernameNotYetCreated => write!(f, "Sora username not yet created. Please create a username in the Sora app or website."),
|
||||
Self::TokenExpiredError => write!(f, "JWT token has expired. Please refresh the token."),
|
||||
Self::TooManyConcurrentTasks => write!(f, "Too many concurrent tasks. Please wait."),
|
||||
Self::UnauthorizedCookieOrBearerExpired => write!(f, "Unauthorized: cookie and/or bearer token expired"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,6 @@
|
||||
#[cfg(test)]
|
||||
pub (crate) mod test_utils;
|
||||
|
||||
// Internal lib
|
||||
pub (crate) mod constants;
|
||||
pub (crate) mod utils_internal;
|
||||
|
||||
// User API
|
||||
pub mod credentials;
|
||||
pub mod creds;
|
||||
pub mod error;
|
||||
pub mod recipes;
|
||||
pub mod requests;
|
||||
pub mod utils;
|
||||
pub mod sora_error;
|
||||
pub mod utils;
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::recipes::utils::maybe_renew_session_jwt::maybe_renew_session_jwt;
|
||||
use crate::requests::generate_sora2_video::generate_sora2_video::{generate_sora2_video, GenerateSora2VideoArgs, GenerateSora2VideoResponse};
|
||||
|
||||
/// Generate Sora 2 video with session auto-renewal.
|
||||
/// If a new sora credential is returned, replace the old one with the new one.
|
||||
pub async fn generate_sora2_video_with_session_auto_renew(
|
||||
args: GenerateSora2VideoArgs<'_>,
|
||||
) -> Result<(GenerateSora2VideoResponse, Option<SoraCredentialSet>), SoraError> {
|
||||
let mut maybe_new_creds = maybe_renew_session_jwt(&args.credentials).await?;
|
||||
|
||||
let use_creds = maybe_new_creds.as_ref()
|
||||
.unwrap_or_else(|| &args.credentials);
|
||||
|
||||
let mut request = args.clone();
|
||||
request.credentials = use_creds;
|
||||
|
||||
let result = generate_sora2_video(request).await;
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok((response, maybe_new_creds)),
|
||||
Err(err) => Err(err), // TODO: Should we retry?
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::recipes::generate_sora2_video::generate_sora2_video_with_session_auto_renew::generate_sora2_video_with_session_auto_renew;
|
||||
use crate::requests::generate_sora2_video::generate_sora2_video::{GenerateSora2VideoArgs, Orientation};
|
||||
use crate::test_utils::get_test_credentials::get_test_credentials;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
#[ignore] // You can manually run "ignore" tests in the IDE, but they won't run in CI.
|
||||
#[tokio::test]
|
||||
pub async fn manual_test() -> AnyhowResult<()> {
|
||||
let creds = get_test_credentials()?;
|
||||
|
||||
let args = GenerateSora2VideoArgs {
|
||||
prompt: "a dog playing tennis",
|
||||
credentials: &creds,
|
||||
request_timeout: None,
|
||||
orientation: Orientation::Landscape,
|
||||
image_reference_media_ids: None,
|
||||
};
|
||||
|
||||
let (result , creds) =
|
||||
generate_sora2_video_with_session_auto_renew(args).await?;
|
||||
|
||||
println!("result: {:#?}", result);
|
||||
assert_eq!(1, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user