Compare commits

..
Author SHA1 Message Date
Brandon Thomas f587e19d25 remove gpu loading error 2025-09-27 18:27:33 -04:00
367 changed files with 3675 additions and 6917 deletions
-3
View File
@@ -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
Generated
+222 -207
View File
File diff suppressed because it is too large Load Diff
+5 -18
View File
@@ -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,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,
}
@@ -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,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;
@@ -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,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;
@@ -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(())
}
}
@@ -1 +0,0 @@
pub mod generate_sora2_video_with_session_auto_renew;
@@ -1,17 +1,17 @@
use std::time::Duration;
use anyhow::anyhow;
use log::{info, warn};
use errors::AnyhowResult;
use crate::creds::credential_migration::CredentialMigrationRef;
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_error::SoraError;
use crate::error::sora_generic_api_error::SoraGenericApiError;
use crate::error::sora_specific_api_error::SoraSpecificApiError;
use crate::requests::auth_bearer::generate_bearer_jwt_with_cookie::generate_bearer_jwt_with_cookie;
use crate::requests::auth_sentinel::generate_sentinel_token::generate_sentinel_token;
use crate::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie;
use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
use crate::requests::image_gen::{image_gen_http_request, SoraImageGenError};
use crate::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info, warn};
use std::time::Duration;
use crate::requests::sentinel_refresh::generate::token::generate_token;
use crate::sora_error::SoraError;
pub struct ImageRemixAutoRenewRequest<'a> {
pub prompt: String,
@@ -31,7 +31,7 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq
num_images: request.num_images,
image_size: request.image_size,
sora_media_tokens: request.sora_media_tokens.clone(), // NB: Clone because used again
credentials: request.credentials,
credentials: CredentialMigrationRef::New(request.credentials),
request_timeout: request.request_timeout,
}).await;
@@ -46,41 +46,36 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq
let mut refresh_sentinel = false;
match err {
// We'll specifically fail these non-retryable requests...
SoraError::ApiSpecific(SoraSpecificApiError::TooManyConcurrentTasks) |
SoraError::ApiSpecific(SoraSpecificApiError::SoraUsernameNotYetCreated) => {
error!("Non-retryable image generation error: {:?}", err);
return Err(err);
// We'll fail these requests...
SoraImageGenError::TooManyConcurrentTasks(_) => {
return Err(SoraError::TooManyConcurrentTasks);
}
SoraImageGenError::GenericError(err) => {
return Err(SoraError::OtherBadStatus(anyhow!("image remix failed with GenericError: {:?}", err)))
}
SoraImageGenError::NetworkError(err) => {
// TODO: The underlying type should be a reqwest::Error.
return Err(SoraError::OtherBadStatus(anyhow!("network error: {:?}", err)))
}
SoraImageGenError::UsernameRequired(_err) => {
return Err(SoraError::SoraUsernameNotYetCreated)
}
// We'll retry these requests...
SoraError::ApiSpecific(SoraSpecificApiError::SentinelBlockError) => {
SoraImageGenError::SentinelBlock(err) => {
warn!("Image generation failed due to sentinel block error: {:?}", err);
refresh_sentinel = true;
}
SoraError::ApiSpecific(SoraSpecificApiError::TokenExpiredError) => {
SoraImageGenError::TokenExpired(err) => {
warn!("Image generation failed due to token expired error: {:?}", err);
refresh_sentinel = true; // TODO: Not sure what this error is, actually.
refresh_jwt = true;
}
SoraError::ApiSpecific(SoraSpecificApiError::InvalidJwt) => {
SoraImageGenError::InvalidJwt(err) => {
warn!("Image generation failed due to invalid jwt error: {:?}", err);
refresh_jwt = true;
}
SoraError::ApiSpecific(SoraSpecificApiError::UnauthorizedCookieOrBearerExpired) => {
warn!("Image generation failed due to auth error (nb: this is a legacy error): {:?}", err);
refresh_jwt = true;
refresh_sentinel = true;
}
// We'll fail everything else eagerly...
_ => {
error!("Image generation error: {:?}", err);
return Err(err);
}
}
let mut new_bearer = None;
@@ -88,15 +83,15 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq
if refresh_jwt {
info!("Generating new JWT bearer token...");
let cookies = request.credentials.cookies.as_str();
let response = generate_bearer_jwt_with_cookie(cookies).await;
let response = generate_bearer_with_cookie(cookies).await;
match response {
Err(err) => {
return Err(SoraGenericApiError::UncategorizedBadResponse(format!("failed to generate new JWT bearer token: {:?}", err)).into())
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new JWT bearer token: {:?}", err)))
}
Ok(bearer) => {
match SoraJwtBearerToken::new(bearer) {
Err(err) => {
return Err(SoraGenericApiError::UncategorizedBadResponse(format!("Failed to parse new JWT bearer token: {:?}", err)).into());
return Err(SoraError::OtherBadStatus(anyhow!("Failed to parse new JWT bearer token: {:?}", err)));
}
Ok(bearer) => new_bearer = Some(bearer),
}
@@ -108,10 +103,10 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq
if refresh_sentinel {
info!("Generating new sentinel...");
let response = generate_sentinel_token().await;
let response = generate_token().await;
match response {
Err(err) => {
return Err(SoraGenericApiError::UncategorizedBadResponse(format!("failed to generate new sentinel: {:?}", err)).into())
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new sentinel: {:?}", err)))
}
Ok(sentinel) => new_sentinel = Some(SoraSentinel::new(sentinel)),
}
@@ -134,9 +129,20 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq
num_images: request.num_images,
image_size: request.image_size,
sora_media_tokens: request.sora_media_tokens,
credentials: &new_creds,
credentials: CredentialMigrationRef::New(&new_creds),
request_timeout: request.request_timeout,
}).await?;
Ok((result, Some(new_creds)))
}).await;
match result {
Ok(response) => Ok((response, Some(new_creds))),
Err(err) => match err {
image_gen_http_request::SoraImageGenError::TooManyConcurrentTasks(err) => {
Err(SoraError::TooManyConcurrentTasks)
}
_ => {
warn!("Image remix failed again: {:?}", err);
Err(SoraError::OtherBadStatus(anyhow!("image remix failed again: {:?}", err)))
}
}
}
}
@@ -1,15 +1,16 @@
use crate::creds::credential_migration::CredentialMigrationRef;
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_error::SoraError;
use crate::recipes::maybe_refresh_credentials_on_sora_error::maybe_refresh_credentials_on_sora_error;
use crate::requests::auth_bearer::generate_bearer_jwt_with_cookie::generate_bearer_jwt_with_cookie;
use crate::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie;
use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
use crate::requests::image_gen::image_gen_http_request;
use crate::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use crate::requests::sentinel_refresh::generate::token::generate_token;
use crate::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use crate::requests::upload::upload_media_http_request::upload_media_http_request;
use crate::requests::upload::upload_media_http_response::SoraMediaUploadResponse;
use crate::requests::upload::upload_media_http_request::{upload_media_http_request, SoraMediaUploadResponse};
use crate::sora_error::SoraError;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info, warn};
@@ -34,7 +35,7 @@ pub async fn image_upload_from_bytes_with_session_auto_renew(
let result = sora_media_upload_from_bytes(
request.file_bytes.clone(), // FIXME(bt): This is horrible, but the client needs to take ownership. :(
request.filename.clone(), // FIXME: Same
request.credentials,
CredentialMigrationRef::New(request.credentials),
request.request_timeout,
).await;
@@ -54,7 +55,7 @@ pub async fn image_upload_from_bytes_with_session_auto_renew(
let result = sora_media_upload_from_bytes(
request.file_bytes,
request.filename,
&new_creds,
CredentialMigrationRef::New(&new_creds),
request.request_timeout,
).await?;
@@ -1,10 +1,9 @@
use crate::creds::credential_migration::CredentialMigrationRef;
use crate::creds::sora_credential_set::SoraCredentialSet;
use crate::error::sora_error::SoraError;
use crate::error::sora_specific_api_error::SoraSpecificApiError;
use crate::recipes::maybe_refresh_credentials_on_sora_error::maybe_refresh_credentials_on_sora_error;
use crate::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use crate::requests::upload::upload_media_http_request::upload_media_http_request;
use crate::requests::upload::upload_media_http_response::SoraMediaUploadResponse;
use crate::requests::upload::upload_media_http_request::{upload_media_http_request, SoraMediaUploadResponse};
use crate::sora_error::SoraError;
use log::{info, warn};
use std::path::Path;
use std::time::Duration;
@@ -25,7 +24,7 @@ pub async fn image_upload_from_file_with_session_auto_renew<P: AsRef<Path>>(
let result = sora_media_upload_from_file(
request.file_path.as_ref().clone(), // FIXME(bt): This is horrible, but the client needs to take ownership. :(
request.credentials,
CredentialMigrationRef::New(request.credentials),
/// This function can try to upload several times. This is the timeout for *individual* requests.
request.request_timeout,
).await;
@@ -33,7 +32,7 @@ pub async fn image_upload_from_file_with_session_auto_renew<P: AsRef<Path>>(
let err = match result {
Ok(response) => return Ok((response, None)),
// We can't retry some errors.
Err(err @ SoraError::ApiSpecific(SoraSpecificApiError::SoraUsernameNotYetCreated)) => return Err(err),
Err(err @ SoraError::SoraUsernameNotYetCreated) => return Err(err),
// Retry all other errors.
Err(err) => err,
};
@@ -46,7 +45,7 @@ pub async fn image_upload_from_file_with_session_auto_renew<P: AsRef<Path>>(
let result = sora_media_upload_from_file(
request.file_path,
&new_creds,
CredentialMigrationRef::New(&new_creds),
request.request_timeout,
).await?;
@@ -1,60 +0,0 @@
use crate::creds::sora_credential_set::SoraCredentialSet;
use crate::error::sora_error::SoraError;
use crate::recipes::image_upload_from_file_with_session_auto_renew::ImageUploadFromFileAutoRenewRequest;
use crate::recipes::maybe_refresh_credentials_on_sora_error::maybe_refresh_credentials_on_sora_error;
use crate::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use crate::requests::common::task_id::TaskId;
use crate::requests::deprecated::job_status::sora_job_status::{get_image_gen_status, StatusRequest, TaskResponse, VideoGenStatusResponse};
use crate::requests::list_classic_tasks::list_classic_tasks::{list_classic_tasks, ListTasksResponse};
use crate::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use anyhow::Error;
use log::{info, warn};
use std::path::Path;
/// Check Sora task statuses with session auto-renewal.
/// If a new sora credential is returned, replace the old one with the new one.
pub async fn list_classic_sora_tasks_with_session_auto_renew(
credentials: &SoraCredentialSet,
) -> Result<(ListTasksResponse, Option<SoraCredentialSet>), SoraError> {
let mut maybe_new_creds = maybe_renew_session(&credentials).await?;
let upgraded_creds = maybe_new_creds.as_ref()
.unwrap_or_else(|| &credentials);
let result = list_classic_tasks(&upgraded_creds).await;
let err = match result {
Ok(response) => return Ok((response, maybe_new_creds)),
Err(err) => err,
};
warn!("Task status polling failed: {:?}", err);
// TODO(bt,2025-05-28): This should only be done if the credentials were known to be bad
let new_creds = maybe_refresh_credentials_on_sora_error(&upgraded_creds, err).await?;
// TODO(bt,2025-05-28): This should only be done if the credentials were actually refreshed.
info!("Retrying task polling with new credentials...");
let result = list_classic_tasks(&new_creds).await?;
Ok((result, Some(new_creds)))
}
async fn maybe_renew_session(creds: &SoraCredentialSet) -> Result<Option<SoraCredentialSet>, SoraError> {
if !creds.jwt_bearer_token.is_none() {
// TODO: Handle JWT expired case as well.
return Ok(None);
}
info!("JWT not set. Upgrading credentials...");
let mut updated_creds = creds.clone();
if let Err(err) = maybe_upgrade_or_renew_session(&mut updated_creds).await {
warn!("Failed to upgrade or renew session: {:?}", err);
return Err(err);
}
Ok(Some(updated_creds))
}
@@ -1,45 +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::list_sora2_drafts::list_sora2_drafts::{list_sora2_drafts, ListSora2DraftsResponse};
/// Check Sora 2 drafts with session auto-renewal.
/// If a new sora credential is returned, replace the old one with the new one.
pub async fn list_sora2_drafts_with_session_auto_renew(
credentials: &SoraCredentialSet,
) -> Result<(ListSora2DraftsResponse, Option<SoraCredentialSet>), SoraError> {
let mut maybe_new_creds = maybe_renew_session_jwt(&credentials).await?;
let use_creds = maybe_new_creds.as_ref()
.unwrap_or_else(|| &credentials);
let result = list_sora2_drafts(&use_creds).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::list_sora2_drafts::list_sora2_drafts_with_session_auto_renew::list_sora2_drafts_with_session_auto_renew;
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 (result , creds) =
list_sora2_drafts_with_session_auto_renew(&creds).await?;
println!("result: {:#?}", result);
assert_eq!(1, 2);
Ok(())
}
}
@@ -1 +0,0 @@
pub mod list_sora2_drafts_with_session_auto_renew;
@@ -0,0 +1,63 @@
use crate::creds::credential_migration::CredentialMigrationRef;
use crate::creds::sora_credential_set::SoraCredentialSet;
use crate::recipes::image_upload_from_file_with_session_auto_renew::ImageUploadFromFileAutoRenewRequest;
use crate::recipes::maybe_refresh_credentials_on_sora_error::maybe_refresh_credentials_on_sora_error;
use crate::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use crate::requests::image_gen::image_gen_status::{get_image_gen_status, StatusRequest, TaskId, TaskResponse, VideoGenStatusResponse};
use crate::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use crate::requests::upload::upload_media_http_request::SoraMediaUploadResponse;
use crate::sora_error::SoraError;
use anyhow::Error;
use log::{info, warn};
use std::path::Path;
pub struct StatusRequestArgs {
pub limit: Option<u32>,
pub before: Option<TaskId>,
pub credentials: SoraCredentialSet,
}
/// Check Sora task statuses with session auto-renewal.
/// If a new sora credential is returned, replace the old one with the new one.
pub async fn list_sora_task_status_with_session_auto_renew(
args: StatusRequestArgs,
) -> Result<(VideoGenStatusResponse, Option<SoraCredentialSet>), SoraError> {
let request = StatusRequest {
limit: args.limit,
before: args.before.map(|t| t.0),
};
let mut new_creds = None;
if args.credentials.jwt_bearer_token.is_none() {
info!("JWT not set. Upgrading credentials...");
let mut updated_creds = args.credentials.clone();
if let Err(err) = maybe_upgrade_or_renew_session(&mut updated_creds).await {
warn!("Failed to upgrade or renew session: {:?}", err);
}
new_creds = Some(updated_creds);
}
let upgraded_creds = new_creds.as_ref()
.unwrap_or_else(|| &args.credentials);
let result = get_image_gen_status(&request, &upgraded_creds).await;
let err = match result {
Ok(response) => return Ok((response, new_creds)),
Err(err) => err,
};
warn!("Task status polling failed: {:?}", err);
// TODO(bt,2025-05-28): This should only be done if the credentials were known to be bad
let new_creds = maybe_refresh_credentials_on_sora_error(&args.credentials, err).await?;
// TODO(bt,2025-05-28): This should only be done if the credentials were actually refreshed.
info!("Retrying task polling with new credentials...");
let result = get_image_gen_status(&request, &upgraded_creds).await?;
Ok((result, Some(new_creds)))
}
@@ -1,9 +1,7 @@
use crate::creds::sora_credential_set::SoraCredentialSet;
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
use crate::error::sora_client_error::SoraClientError;
use crate::error::sora_error::SoraError;
use crate::error::sora_specific_api_error::SoraSpecificApiError;
use crate::requests::auth_bearer::generate_bearer_jwt_with_cookie::generate_bearer_jwt_with_cookie;
use crate::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie;
use crate::sora_error::SoraError;
use anyhow::anyhow;
use log::{error, info, warn};
@@ -12,33 +10,31 @@ use log::{error, info, warn};
pub async fn maybe_refresh_credentials_on_sora_error(creds: &SoraCredentialSet, error: SoraError) -> Result<SoraCredentialSet, SoraError> {
match error {
SoraError::Client(SoraClientError::NoBearerTokenForRequest) => {
error!("Previous request failed due to missing bearer token.");
SoraError::NoBearerTokenAvailable => {
warn!("Previous request failed due to missing bearer token.");
}
SoraError::ApiSpecific(SoraSpecificApiError::UnauthorizedCookieOrBearerExpired) => {
error!("Previous request failed due to invalid bearer token.");
SoraError::UnauthorizedCookieOrBearerExpired => {
warn!("Previous request failed due to invalid bearer token.");
}
_ => {
error!("Previous request failed due to the following error: {:?}", error);
return Err(error)
error!("Previous request failed with error: {:?}", error);
return Err(SoraError::OtherBadStatus(anyhow!("previous request failed with error: {:?}", error)))
}
}
info!("Generating new JWT bearer token...");
let cookies = creds.cookies.as_str();
let response = generate_bearer_jwt_with_cookie(cookies).await;
let response = generate_bearer_with_cookie(cookies).await;
let new_bearer = match response {
Err(err) => {
error!("Error generating new JWT bearer token: {}", err);
return Err(err);
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new JWT bearer token: {:?}", err)))
}
Ok(bearer) => {
match SoraJwtBearerToken::new(bearer) {
Err(err) => {
error!("Error parsing new JWT bearer token: {}", err);
return Err(err);
return Err(SoraError::OtherBadStatus(anyhow!("failed to parse new JWT bearer token: {:?}", err)));
}
Ok(bearer) => bearer,
}
@@ -1,20 +1,19 @@
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_error::SoraError;
use crate::requests::auth_bearer::generate_bearer_jwt_with_cookie::generate_bearer_jwt_with_cookie;
use crate::requests::auth_sentinel::generate_sentinel_token::generate_sentinel_token;
use crate::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie;
use crate::requests::sentinel_refresh::generate::token::generate_token;
use chrono::{DateTime, TimeDelta, Utc};
use errors::AnyhowResult;
use log::info;
use std::ops::Sub;
use crate::sora_error::SoraError;
const EXPIRATION_DEADLINE : TimeDelta = TimeDelta::hours(12);
/// Call this at session startup to generate the tokens for the first time.
/// Call this periodically to refresh the JWT as it nears its expiration date.
/// This will not refresh the sentinel token once it's been generated - we can use other flows to accomplish that.
#[deprecated(note="The sentinel appears to be going away. Use `maybe_renew_session_jwt` instead for newer '2.0' code.")]
pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentialSet) -> Result<bool, SoraError> {
let mut refresh_jwt = !sora_credentials.jwt_bearer_token.is_some();
let refresh_sentinel = !sora_credentials.sora_sentinel.is_some();
@@ -35,7 +34,7 @@ pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentia
if refresh_jwt {
let cookies = sora_credentials.cookies.as_str();
let token = generate_bearer_jwt_with_cookie(cookies).await?;
let token = generate_bearer_with_cookie(cookies).await?;
info!("Parsing JWT bearer token.");
let token = SoraJwtBearerToken::new(token)?;
@@ -45,7 +44,7 @@ pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentia
if refresh_sentinel {
info!("Generating new sentinel...");
let token = generate_sentinel_token().await?;
let token = generate_token().await?;
let token = SoraSentinel::new(token);
sora_credentials.sora_sentinel = Some(token);
credential_updated = true;
@@ -1,11 +1,8 @@
pub mod generate_sora2_video;
pub mod image_remix_with_session_auto_renew;
pub mod image_upload_from_bytes_with_session_auto_renew;
pub mod image_upload_from_file_with_session_auto_renew;
pub mod list_classic_sora_tasks_with_session_auto_renew;
pub mod list_sora2_drafts;
pub mod list_sora_task_status_with_session_auto_renew;
pub mod maybe_refresh_credentials_on_sora_error;
pub mod maybe_upgrade_or_renew_session;
pub mod simple_image_gen_with_session_auto_renew;
pub mod utils;
pub mod wait_for_image_gen_status;
@@ -1,17 +1,17 @@
use crate::creds::credential_migration::CredentialMigrationRef;
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_error::SoraError;
use crate::error::sora_generic_api_error::SoraGenericApiError;
use crate::error::sora_specific_api_error::SoraSpecificApiError;
use crate::requests::auth_bearer::generate_bearer_jwt_with_cookie::generate_bearer_jwt_with_cookie;
use crate::requests::auth_sentinel::generate_sentinel_token::generate_sentinel_token;
use crate::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie;
use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
use crate::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use crate::requests::image_gen::sora_image_gen_simple::{sora_image_gen_simple, SoraImageGenSimpleRequest};
use crate::requests::image_gen::{image_gen_http_request, SoraImageGenError};
use crate::requests::sentinel_refresh::generate::token::generate_token;
use crate::sora_error::SoraError;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info, warn};
use log::{info, warn};
use std::time::Duration;
pub struct SimpleImageGenAutoRenewRequest<'a> {
@@ -30,7 +30,7 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut
prompt: request.prompt.clone(), // NB: Clone because used again
num_images: request.num_images,
image_size: request.image_size,
credentials: request.credentials,
credentials: CredentialMigrationRef::New(request.credentials),
request_timeout: request.request_timeout,
}).await;
@@ -45,41 +45,36 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut
let mut refresh_sentinel = false;
match err {
// We'll specifically fail these non-retryable requests...
SoraError::ApiSpecific(SoraSpecificApiError::TooManyConcurrentTasks) |
SoraError::ApiSpecific(SoraSpecificApiError::SoraUsernameNotYetCreated) => {
error!("Non-retryable image generation error: {:?}", err);
return Err(err);
// We'll fail these requests...
SoraImageGenError::TooManyConcurrentTasks(_) => {
return Err(SoraError::TooManyConcurrentTasks);
}
SoraImageGenError::GenericError(err) => {
return Err(SoraError::OtherBadStatus(anyhow!("image gen failed with GenericError: {:?}", err)))
}
SoraImageGenError::NetworkError(err) => {
// TODO: The underlying type should be a reqwest::Error.
return Err(SoraError::OtherBadStatus(anyhow!("network error: {:?}", err)))
}
SoraImageGenError::UsernameRequired(_err) => {
return Err(SoraError::SoraUsernameNotYetCreated)
}
// We'll retry these requests...
SoraError::ApiSpecific(SoraSpecificApiError::SentinelBlockError) => {
SoraImageGenError::SentinelBlock(err) => {
warn!("Image generation failed due to sentinel block error: {:?}", err);
refresh_sentinel = true;
}
SoraError::ApiSpecific(SoraSpecificApiError::TokenExpiredError) => {
SoraImageGenError::TokenExpired(err) => {
warn!("Image generation failed due to token expired error: {:?}", err);
refresh_sentinel = true; // TODO: Not sure what this error is, actually.
refresh_jwt = true;
}
SoraError::ApiSpecific(SoraSpecificApiError::InvalidJwt) => {
SoraImageGenError::InvalidJwt(err) => {
warn!("Image generation failed due to invalid jwt error: {:?}", err);
refresh_jwt = true;
}
SoraError::ApiSpecific(SoraSpecificApiError::UnauthorizedCookieOrBearerExpired) => {
warn!("Image generation failed due to auth error (nb: this is a legacy error): {:?}", err);
refresh_jwt = true;
refresh_sentinel = true;
}
// We'll fail everything else eagerly...
_ => {
error!("Image generation error: {:?}", err);
return Err(err);
}
}
let mut new_bearer = None;
@@ -87,17 +82,15 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut
if refresh_jwt {
info!("Generating new JWT bearer token...");
let cookies = request.credentials.cookies.as_str();
let response = generate_bearer_jwt_with_cookie(cookies).await;
let response = generate_bearer_with_cookie(cookies).await;
match response {
Err(err) => {
error!("Error asking to generate JWT: {:?}", err);
return Err(err);
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new JWT bearer token: {:?}", err)))
}
Ok(bearer) => {
match SoraJwtBearerToken::new(bearer) {
Err(err) => {
error!("Failed to parse new JWT: {:?}", err);
return Err(err);
return Err(SoraError::OtherBadStatus(anyhow!("Failed to parse new JWT bearer token: {:?}", err)));
}
Ok(bearer) => new_bearer = Some(bearer),
}
@@ -109,11 +102,10 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut
if refresh_sentinel {
info!("Generating new sentinel...");
let response = generate_sentinel_token().await;
let response = generate_token().await;
match response {
Err(err) => {
error!("failed to generate new sentinel: {:?}", err);
return Err(err)
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new sentinel: {:?}", err)))
}
Ok(sentinel) => new_sentinel = Some(SoraSentinel::new(sentinel)),
}
@@ -135,9 +127,20 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut
prompt: request.prompt,
num_images: request.num_images,
image_size: request.image_size,
credentials: &new_creds,
credentials: CredentialMigrationRef::New(&new_creds),
request_timeout: request.request_timeout,
}).await?;
Ok((result, Some(new_creds)))
}).await;
match result {
Ok(response) => Ok((response, Some(new_creds))),
Err(err) => match err {
image_gen_http_request::SoraImageGenError::TooManyConcurrentTasks(err) => {
Err(SoraError::TooManyConcurrentTasks)
}
_ => {
warn!("Image gen failed again: {:?}", err);
Err(SoraError::OtherBadStatus(anyhow!("image gen failed again: {:?}", err)))
}
}
}
}
@@ -1,48 +0,0 @@
use crate::creds::sora_credential_set::SoraCredentialSet;
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
use crate::error::sora_error::SoraError;
use crate::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use crate::requests::auth_bearer::generate_bearer_jwt_with_cookie::generate_bearer_jwt_with_cookie;
use chrono::{TimeDelta, Utc};
use log::{info, warn};
use std::ops::Sub;
const EXPIRATION_DEADLINE : TimeDelta = TimeDelta::hours(12);
/// Returns a new set of credentials if they were updated, or None if no update was needed.
pub async fn maybe_renew_session_jwt(creds: &SoraCredentialSet) -> Result<Option<SoraCredentialSet>, SoraError> {
let mut refresh_jwt = false;
match creds.jwt_bearer_token.as_ref() {
None => {
info!("JWT not set.");
refresh_jwt = true;
}
Some(jwt) => {
let now = Utc::now();
let refresh_deadline = now.sub(EXPIRATION_DEADLINE);
if jwt.jwt_claims().expiration.lt(&now) {
info!("JWT expiration time has elapsed.");
refresh_jwt = true;
} else if jwt.jwt_claims().expiration.lt(&refresh_deadline) {
info!("JWT expiration time is under the expiration deadline.");
refresh_jwt = true;
}
}
}
if !refresh_jwt {
return Ok(None);
}
info!("Refreshing JWT...");
let token = generate_bearer_jwt_with_cookie(creds.cookies.as_str()).await?;
info!("Parsing JWT bearer token...");
let token = SoraJwtBearerToken::new(token)?;
let mut updated_creds = creds.clone();
updated_creds.jwt_bearer_token = Some(token);
Ok(Some(updated_creds))
}
@@ -1 +0,0 @@
pub mod maybe_renew_session_jwt;
@@ -1,8 +1,10 @@
use crate::creds::sora_credential_set::SoraCredentialSet;
use crate::requests::deprecated::job_status::sora_job_status::{get_image_gen_status, StatusRequest, TaskResponse, TaskStatus, VideoGenStatusResponse};
use crate::credentials::SoraCredentials;
use crate::requests::image_gen::image_gen_status::TaskStatus;
use crate::requests::image_gen::image_gen_status::{get_image_gen_status, StatusRequest, TaskResponse, VideoGenStatusResponse};
use errors::AnyhowResult;
use crate::creds::sora_credential_set::SoraCredentialSet;
pub async fn wait_for_image_gen_status(task_id: &String, creds: &SoraCredentialSet, retry_limit: Option<u32>) -> AnyhowResult<TaskResponse> {
pub async fn wait_for_image_gen_status(task_id: &String, credentials: &SoraCredentials, retry_limit: Option<u32>) -> AnyhowResult<TaskResponse> {
let status_request = StatusRequest {
limit: None,
before: None,
@@ -10,10 +12,12 @@ pub async fn wait_for_image_gen_status(task_id: &String, creds: &SoraCredentialS
};
let retry_limit = retry_limit.unwrap_or(10);
let new_creds = SoraCredentialSet::from_legacy_credentials(&credentials)?;
for _ in 0..retry_limit {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
let status_response = get_image_gen_status(&status_request, &creds).await?;
let status_response = get_image_gen_status(&status_request, &new_creds).await?;
match find_task_response_by_id(&status_response, task_id.clone()) {
Some(task_response) => match TaskStatus::from_str(&task_response.status) {
@@ -1 +0,0 @@
pub mod generate_bearer_jwt_with_cookie;

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