mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-01 00:05:54 -04:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
350968e0c9 | ||
|
|
c6bf6101ff | ||
|
|
0190645e50 | ||
|
|
4d662e1347 | ||
|
|
173ef4e977 | ||
|
|
b1cf0bc1b9 | ||
|
|
c30308d43c | ||
|
|
781d161aed | ||
|
|
2cc2af83c7 | ||
|
|
2fbd2c8f8b | ||
|
|
2d3ec8b4e5 | ||
|
|
a96db56d8f | ||
|
|
3c4179f31b |
Generated
+217
-230
File diff suppressed because it is too large
Load Diff
+14
-1
@@ -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.22", features = ["json", "cookies"] }
|
||||
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
|
||||
reqwest-websocket = { version = "0.5.0" }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_derive = "1.0.219"
|
||||
@@ -255,3 +255,16 @@ 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"
|
||||
|
||||
@@ -24,27 +24,12 @@ 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.workspace = true
|
||||
url.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"] }
|
||||
url.workspace = true
|
||||
wreq-util.workspace = true
|
||||
wreq.workspace = true
|
||||
|
||||
# NB: We need to parse HTML to get out the websocket token
|
||||
dom_query = "0.20.2"
|
||||
|
||||
@@ -28,14 +28,19 @@ 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" }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub (crate) mod user_agent;
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
// 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";
|
||||
@@ -1,60 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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;
|
||||
@@ -0,0 +1,76 @@
|
||||
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,7 +1,5 @@
|
||||
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;
|
||||
|
||||
@@ -38,38 +36,16 @@ impl SoraCredentialSet {
|
||||
SoraCookies::new(cookies.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
// 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()));
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
Ok(SoraCredentialSet {
|
||||
cookies,
|
||||
jwt_bearer_token: Some(jwt_bearer_token),
|
||||
sora_sentinel,
|
||||
})
|
||||
if let Some(sentinel) = &self.sora_sentinel {
|
||||
builder = builder.with_sora_sentinel(sentinel.as_str());
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
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::utils::lightweight_sora_jwt_parse::{lightweight_sora_jwt_parse, SoraJwtClaims};
|
||||
use errors::AnyhowResult;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::utils_internal::lightweight_sora_jwt_parse::{lightweight_sora_jwt_parse, SoraJwtClaims};
|
||||
|
||||
/// 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) -> AnyhowResult<Self> {
|
||||
pub fn new(token: String) -> Result<Self, SoraError> {
|
||||
let jwt_claims = lightweight_sora_jwt_parse(&token)?;
|
||||
Ok(SoraJwtBearerToken {
|
||||
token,
|
||||
@@ -20,6 +20,10 @@ 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,6 +17,10 @@ impl SoraSentinel {
|
||||
&self.sentinel
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.sentinel
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.sentinel.as_bytes()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod sora_client_error;
|
||||
pub mod sora_error;
|
||||
pub mod sora_generic_api_error;
|
||||
pub mod sora_specific_api_error;
|
||||
@@ -0,0 +1,53 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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,6 +1,7 @@
|
||||
pub mod credentials;
|
||||
pub (crate) mod constants;
|
||||
pub (crate) mod utils_internal;
|
||||
pub mod creds;
|
||||
pub mod error;
|
||||
pub mod recipes;
|
||||
pub mod requests;
|
||||
pub mod sora_error;
|
||||
pub mod utils;
|
||||
pub mod utils;
|
||||
|
||||
+40
-46
@@ -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::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie;
|
||||
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::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 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 std::time::Duration;
|
||||
|
||||
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: CredentialMigrationRef::New(request.credentials),
|
||||
credentials: request.credentials,
|
||||
request_timeout: request.request_timeout,
|
||||
}).await;
|
||||
|
||||
@@ -46,36 +46,41 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq
|
||||
let mut refresh_sentinel = false;
|
||||
|
||||
match 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 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 retry these requests...
|
||||
|
||||
SoraImageGenError::SentinelBlock(err) => {
|
||||
SoraError::ApiSpecific(SoraSpecificApiError::SentinelBlockError) => {
|
||||
warn!("Image generation failed due to sentinel block error: {:?}", err);
|
||||
refresh_sentinel = true;
|
||||
}
|
||||
SoraImageGenError::TokenExpired(err) => {
|
||||
SoraError::ApiSpecific(SoraSpecificApiError::TokenExpiredError) => {
|
||||
warn!("Image generation failed due to token expired error: {:?}", err);
|
||||
refresh_sentinel = true; // TODO: Not sure what this error is, actually.
|
||||
refresh_jwt = true;
|
||||
}
|
||||
SoraImageGenError::InvalidJwt(err) => {
|
||||
SoraError::ApiSpecific(SoraSpecificApiError::InvalidJwt) => {
|
||||
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;
|
||||
@@ -83,15 +88,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_with_cookie(cookies).await;
|
||||
let response = generate_bearer_jwt_with_cookie(cookies).await;
|
||||
match response {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new JWT bearer token: {:?}", err)))
|
||||
return Err(SoraGenericApiError::UncategorizedBadResponse(format!("failed to generate new JWT bearer token: {:?}", err)).into())
|
||||
}
|
||||
Ok(bearer) => {
|
||||
match SoraJwtBearerToken::new(bearer) {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("Failed to parse new JWT bearer token: {:?}", err)));
|
||||
return Err(SoraGenericApiError::UncategorizedBadResponse(format!("Failed to parse new JWT bearer token: {:?}", err)).into());
|
||||
}
|
||||
Ok(bearer) => new_bearer = Some(bearer),
|
||||
}
|
||||
@@ -103,10 +108,10 @@ pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewReq
|
||||
|
||||
if refresh_sentinel {
|
||||
info!("Generating new sentinel...");
|
||||
let response = generate_token().await;
|
||||
let response = generate_sentinel_token().await;
|
||||
match response {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new sentinel: {:?}", err)))
|
||||
return Err(SoraGenericApiError::UncategorizedBadResponse(format!("failed to generate new sentinel: {:?}", err)).into())
|
||||
}
|
||||
Ok(sentinel) => new_sentinel = Some(SoraSentinel::new(sentinel)),
|
||||
}
|
||||
@@ -129,20 +134,9 @@ 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: CredentialMigrationRef::New(&new_creds),
|
||||
credentials: &new_creds,
|
||||
request_timeout: request.request_timeout,
|
||||
}).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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}).await?;
|
||||
|
||||
Ok((result, Some(new_creds)))
|
||||
}
|
||||
|
||||
+4
-6
@@ -1,16 +1,14 @@
|
||||
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::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie;
|
||||
use crate::requests::auth_bearer::generate_bearer_jwt_with_cookie::generate_bearer_jwt_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, SoraMediaUploadResponse};
|
||||
use crate::sora_error::SoraError;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info, warn};
|
||||
@@ -35,7 +33,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
|
||||
CredentialMigrationRef::New(request.credentials),
|
||||
request.credentials,
|
||||
request.request_timeout,
|
||||
).await;
|
||||
|
||||
@@ -55,7 +53,7 @@ pub async fn image_upload_from_bytes_with_session_auto_renew(
|
||||
let result = sora_media_upload_from_bytes(
|
||||
request.file_bytes,
|
||||
request.filename,
|
||||
CredentialMigrationRef::New(&new_creds),
|
||||
&new_creds,
|
||||
request.request_timeout,
|
||||
).await?;
|
||||
|
||||
|
||||
+5
-5
@@ -1,9 +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, SoraMediaUploadResponse};
|
||||
use crate::sora_error::SoraError;
|
||||
use log::{info, warn};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
@@ -24,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. :(
|
||||
CredentialMigrationRef::New(request.credentials),
|
||||
request.credentials,
|
||||
/// This function can try to upload several times. This is the timeout for *individual* requests.
|
||||
request.request_timeout,
|
||||
).await;
|
||||
@@ -32,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::SoraUsernameNotYetCreated) => return Err(err),
|
||||
Err(err @ SoraError::ApiSpecific(SoraSpecificApiError::SoraUsernameNotYetCreated)) => return Err(err),
|
||||
// Retry all other errors.
|
||||
Err(err) => err,
|
||||
};
|
||||
@@ -45,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,
|
||||
CredentialMigrationRef::New(&new_creds),
|
||||
&new_creds,
|
||||
request.request_timeout,
|
||||
).await?;
|
||||
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
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::image_gen::image_gen_status::{get_image_gen_status, StatusRequest, TaskId, TaskResponse, VideoGenStatusResponse};
|
||||
use crate::requests::common::task_id::TaskId;
|
||||
use crate::requests::job_status::sora_job_status::{get_image_gen_status, StatusRequest, 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;
|
||||
|
||||
+15
-11
@@ -1,7 +1,9 @@
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use crate::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie;
|
||||
use crate::sora_error::SoraError;
|
||||
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 anyhow::anyhow;
|
||||
use log::{error, info, warn};
|
||||
|
||||
@@ -10,31 +12,33 @@ use log::{error, info, warn};
|
||||
pub async fn maybe_refresh_credentials_on_sora_error(creds: &SoraCredentialSet, error: SoraError) -> Result<SoraCredentialSet, SoraError> {
|
||||
|
||||
match error {
|
||||
SoraError::NoBearerTokenAvailable => {
|
||||
warn!("Previous request failed due to missing bearer token.");
|
||||
SoraError::Client(SoraClientError::NoBearerTokenForRequest) => {
|
||||
error!("Previous request failed due to missing bearer token.");
|
||||
}
|
||||
SoraError::UnauthorizedCookieOrBearerExpired => {
|
||||
warn!("Previous request failed due to invalid bearer token.");
|
||||
SoraError::ApiSpecific(SoraSpecificApiError::UnauthorizedCookieOrBearerExpired) => {
|
||||
error!("Previous request failed due to invalid bearer token.");
|
||||
}
|
||||
_ => {
|
||||
error!("Previous request failed with error: {:?}", error);
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("previous request failed with error: {:?}", error)))
|
||||
error!("Previous request failed due to the following error: {:?}", error);
|
||||
return Err(error)
|
||||
}
|
||||
}
|
||||
|
||||
info!("Generating new JWT bearer token...");
|
||||
|
||||
let cookies = creds.cookies.as_str();
|
||||
let response = generate_bearer_with_cookie(cookies).await;
|
||||
let response = generate_bearer_jwt_with_cookie(cookies).await;
|
||||
|
||||
let new_bearer = match response {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new JWT bearer token: {:?}", err)))
|
||||
error!("Error generating new JWT bearer token: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
Ok(bearer) => {
|
||||
match SoraJwtBearerToken::new(bearer) {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("failed to parse new JWT bearer token: {:?}", err)));
|
||||
error!("Error parsing new JWT bearer token: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
Ok(bearer) => bearer,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use crate::creds::sora_sentinel::SoraSentinel;
|
||||
use crate::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie;
|
||||
use crate::requests::sentinel_refresh::generate::token::generate_token;
|
||||
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 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);
|
||||
|
||||
@@ -34,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_with_cookie(cookies).await?;
|
||||
let token = generate_bearer_jwt_with_cookie(cookies).await?;
|
||||
|
||||
info!("Parsing JWT bearer token.");
|
||||
let token = SoraJwtBearerToken::new(token)?;
|
||||
@@ -44,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_token().await?;
|
||||
let token = generate_sentinel_token().await?;
|
||||
let token = SoraSentinel::new(token);
|
||||
sora_credentials.sora_sentinel = Some(token);
|
||||
credential_updated = true;
|
||||
|
||||
+42
-45
@@ -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::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie;
|
||||
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::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::{info, warn};
|
||||
use log::{error, 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: CredentialMigrationRef::New(request.credentials),
|
||||
credentials: request.credentials,
|
||||
request_timeout: request.request_timeout,
|
||||
}).await;
|
||||
|
||||
@@ -45,36 +45,41 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut
|
||||
let mut refresh_sentinel = false;
|
||||
|
||||
match err {
|
||||
// We'll fail these requests...
|
||||
SoraImageGenError::TooManyConcurrentTasks(_) => {
|
||||
return Err(SoraError::TooManyConcurrentTasks);
|
||||
// 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);
|
||||
}
|
||||
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...
|
||||
|
||||
SoraImageGenError::SentinelBlock(err) => {
|
||||
|
||||
SoraError::ApiSpecific(SoraSpecificApiError::SentinelBlockError) => {
|
||||
warn!("Image generation failed due to sentinel block error: {:?}", err);
|
||||
refresh_sentinel = true;
|
||||
}
|
||||
SoraImageGenError::TokenExpired(err) => {
|
||||
SoraError::ApiSpecific(SoraSpecificApiError::TokenExpiredError) => {
|
||||
warn!("Image generation failed due to token expired error: {:?}", err);
|
||||
refresh_sentinel = true; // TODO: Not sure what this error is, actually.
|
||||
refresh_jwt = true;
|
||||
}
|
||||
SoraImageGenError::InvalidJwt(err) => {
|
||||
SoraError::ApiSpecific(SoraSpecificApiError::InvalidJwt) => {
|
||||
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;
|
||||
@@ -82,15 +87,17 @@ 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_with_cookie(cookies).await;
|
||||
let response = generate_bearer_jwt_with_cookie(cookies).await;
|
||||
match response {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new JWT bearer token: {:?}", err)))
|
||||
error!("Error asking to generate JWT: {:?}", err);
|
||||
return Err(err);
|
||||
}
|
||||
Ok(bearer) => {
|
||||
match SoraJwtBearerToken::new(bearer) {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("Failed to parse new JWT bearer token: {:?}", err)));
|
||||
error!("Failed to parse new JWT: {:?}", err);
|
||||
return Err(err);
|
||||
}
|
||||
Ok(bearer) => new_bearer = Some(bearer),
|
||||
}
|
||||
@@ -102,10 +109,11 @@ pub async fn simple_image_gen_with_session_auto_renew(request: SimpleImageGenAut
|
||||
|
||||
if refresh_sentinel {
|
||||
info!("Generating new sentinel...");
|
||||
let response = generate_token().await;
|
||||
let response = generate_sentinel_token().await;
|
||||
match response {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new sentinel: {:?}", err)))
|
||||
error!("failed to generate new sentinel: {:?}", err);
|
||||
return Err(err)
|
||||
}
|
||||
Ok(sentinel) => new_sentinel = Some(SoraSentinel::new(sentinel)),
|
||||
}
|
||||
@@ -127,20 +135,9 @@ 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: CredentialMigrationRef::New(&new_creds),
|
||||
credentials: &new_creds,
|
||||
request_timeout: request.request_timeout,
|
||||
}).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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}).await?;
|
||||
|
||||
Ok((result, Some(new_creds)))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
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;
|
||||
use crate::requests::job_status::sora_job_status::{get_image_gen_status, StatusRequest, TaskResponse, TaskStatus, VideoGenStatusResponse};
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub async fn wait_for_image_gen_status(task_id: &String, credentials: &SoraCredentials, retry_limit: Option<u32>) -> AnyhowResult<TaskResponse> {
|
||||
pub async fn wait_for_image_gen_status(task_id: &String, creds: &SoraCredentialSet, retry_limit: Option<u32>) -> AnyhowResult<TaskResponse> {
|
||||
let status_request = StatusRequest {
|
||||
limit: None,
|
||||
before: None,
|
||||
@@ -12,12 +10,10 @@ pub async fn wait_for_image_gen_status(task_id: &String, credentials: &SoraCrede
|
||||
};
|
||||
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, &new_creds).await?;
|
||||
let status_response = get_image_gen_status(&status_request, &creds).await?;
|
||||
|
||||
match find_task_response_by_id(&status_response, task_id.clone()) {
|
||||
Some(task_response) => match TaskStatus::from_str(&task_response.status) {
|
||||
|
||||
+29
-15
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
pub mod generate_bearer_jwt_with_cookie;
|
||||
+7
-7
@@ -1,3 +1,5 @@
|
||||
use rand::Rng;
|
||||
|
||||
// cores = [8, 16, 24, 32]
|
||||
// cached_scripts = ["https://cdn.oaistatic.com/_next/static/cXh69klOLzS0Gy2joLDRS/_ssgManifest.js?dpl=453ebaec0d44c2decab71692e1bfe39be35a24b3"]
|
||||
|
||||
@@ -356,7 +358,6 @@
|
||||
// "__intercomReloadLocation"
|
||||
// ]
|
||||
|
||||
use rand::Rng;
|
||||
|
||||
const CORES: &[usize] = &[8, 16, 24, 32];
|
||||
|
||||
@@ -654,18 +655,17 @@ const WINDOW_KEY: &[&str] = &[
|
||||
];
|
||||
|
||||
pub fn get_random_core_count() -> usize {
|
||||
return CORES[rand::rng().random_range(0..CORES.len())];
|
||||
CORES[rand::rng().random_range(0..CORES.len())]
|
||||
}
|
||||
|
||||
|
||||
pub fn get_random_navigator_key() -> String {
|
||||
return NAVIGATOR_KEY[rand::rng().random_range(0..NAVIGATOR_KEY.len())].to_string();
|
||||
NAVIGATOR_KEY[rand::rng().random_range(0..NAVIGATOR_KEY.len())].to_string()
|
||||
}
|
||||
|
||||
pub fn get_random_document_key() -> String {
|
||||
return DOCUMENT_KEY[rand::rng().random_range(0..DOCUMENT_KEY.len())].to_string();
|
||||
DOCUMENT_KEY[rand::rng().random_range(0..DOCUMENT_KEY.len())].to_string()
|
||||
}
|
||||
|
||||
pub fn get_random_window_key() -> String {
|
||||
return WINDOW_KEY[rand::rng().random_range(0..WINDOW_KEY.len())].to_string();
|
||||
}
|
||||
WINDOW_KEY[rand::rng().random_range(0..WINDOW_KEY.len())].to_string()
|
||||
}
|
||||
+31
-15
@@ -1,17 +1,18 @@
|
||||
use super::request::GenerateSentinelRefreshRequest;
|
||||
use crate::sora_error::SoraError;
|
||||
use crate::utils::classify_general_http_error::classify_general_http_error;
|
||||
use crate::constants::user_agent::USER_AGENT;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::error::sora_generic_api_error::SoraGenericApiError;
|
||||
use crate::requests::list_media::list_media::ListMediaResponse;
|
||||
use crate::utils_internal::classify_general_http_error::classify_general_http_error;
|
||||
use errors::AnyhowResult;
|
||||
use idempotency::uuid::generate_random_uuid;
|
||||
use log::error;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use wreq::Client;
|
||||
|
||||
const SORA_IMAGE_GEN_URL: &str = "https://chatgpt.com/backend-api/sentinel/req";
|
||||
|
||||
/// 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";
|
||||
|
||||
const SENTINEL_FLOW: &str = "sora_create_task";
|
||||
|
||||
|
||||
@@ -43,16 +44,20 @@ pub struct SentinelResponse {
|
||||
}
|
||||
|
||||
|
||||
pub async fn generate_token() -> Result<String, SoraError> {
|
||||
pub async fn generate_sentinel_token() -> Result<String, SoraError> {
|
||||
let (_request, base64_request) = GenerateSentinelRefreshRequest::new().with_fourth_and_tenth();
|
||||
let request = SentinelRequest::new(base64_request);
|
||||
let client = reqwest::Client::new();
|
||||
let client = Client::new();
|
||||
let response = client.post(SORA_IMAGE_GEN_URL)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Client failed to generate sentinel token: {:?}", err);
|
||||
SoraGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
error!("Failed to generate sentinel: {}", response.status());
|
||||
@@ -60,8 +65,19 @@ pub async fn generate_token() -> Result<String, SoraError> {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let response_json: SentinelResponse = response.json().await?;
|
||||
Ok(response_json.token)
|
||||
let text_body = &response.text().await
|
||||
.map_err(|err| {
|
||||
error!("sora error reading sentinel token text body: {}", err);
|
||||
SoraGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let response = serde_json::from_str::<SentinelResponse>(&text_body)
|
||||
.map_err(|err| {
|
||||
error!("Failed to parse media list response: {}", err);
|
||||
SoraGenericApiError::SerdeResponseParseErrorWithBody(err, text_body.to_string())
|
||||
})?;
|
||||
|
||||
Ok(response.token)
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +87,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_generate_token() {
|
||||
let token = generate_token().await.unwrap();
|
||||
let token = generate_sentinel_token().await.unwrap();
|
||||
println!("{}", token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod generate_sentinel_token;
|
||||
pub mod request;
|
||||
pub(crate) mod config;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use crate::requests::sentinel_refresh::generate::config::{get_random_core_count, get_random_document_key, get_random_navigator_key, get_random_window_key};
|
||||
use crate::requests::auth_sentinel::config::{get_random_core_count, get_random_document_key, get_random_navigator_key, get_random_window_key};
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use chrono::FixedOffset;
|
||||
@@ -1 +0,0 @@
|
||||
pub mod generate_bearer_with_cookie;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod task_id;
|
||||
@@ -0,0 +1,12 @@
|
||||
use std::fmt::Display;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// A strongly typed task ID for Sora tasks.
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
|
||||
pub struct TaskId(pub String);
|
||||
|
||||
impl Display for TaskId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::requests::image_gen::image_gen_status::TaskId;
|
||||
use crate::requests::common::task_id::TaskId;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SoraImageGenResponse {
|
||||
|
||||
+52
-71
@@ -1,39 +1,18 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::requests::image_gen::image_gen_status::TaskId;
|
||||
use log::warn;
|
||||
use crate::constants::user_agent::USER_AGENT;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::error::sora_client_error::SoraClientError;
|
||||
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::common::task_id::TaskId;
|
||||
use log::{error, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use wreq::{Client, StatusCode};
|
||||
|
||||
const SORA_IMAGE_GEN_URL: &str = "https://sora.com/backend/video_gen";
|
||||
|
||||
/// 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";
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SoraImageGenError {
|
||||
#[error("Sora token expired: {0}")]
|
||||
TokenExpired(String),
|
||||
|
||||
#[error("Sora sentinel block: {0}")]
|
||||
SentinelBlock(String),
|
||||
|
||||
#[error("Sora too many concurrent tasks: {0}")]
|
||||
TooManyConcurrentTasks(String),
|
||||
|
||||
#[error("Sora invalid JWT: {0}")]
|
||||
InvalidJwt(String),
|
||||
|
||||
#[error("Sora API error: {0}")]
|
||||
GenericError(String),
|
||||
|
||||
#[error("Network error: {0}")]
|
||||
NetworkError(String),
|
||||
|
||||
#[error("Sora username required: {0}")]
|
||||
UsernameRequired(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub (crate) enum VideoGenType {
|
||||
@@ -155,36 +134,20 @@ impl std::fmt::Display for RawSoraErrorInner {
|
||||
/// Don't expose the internal request implementation as there are only a few "correct" ways to call the API.
|
||||
pub (crate) async fn image_gen_http_request(
|
||||
sora_request: RawSoraImageGenRequest,
|
||||
credentials: CredentialMigrationRef<'_>,
|
||||
credentials: &SoraCredentialSet,
|
||||
request_timeout: Option<Duration>,
|
||||
) -> Result<RawSoraResponse, SoraImageGenError> {
|
||||
let client = reqwest::Client::new();
|
||||
) -> Result<RawSoraResponse, SoraError> {
|
||||
let client = Client::new();
|
||||
|
||||
let mut cookie;
|
||||
let mut authorization_header;
|
||||
let mut sentinel;
|
||||
let cookie = credentials.cookies.to_string();
|
||||
|
||||
match credentials {
|
||||
CredentialMigrationRef::Legacy(creds) => {
|
||||
cookie = creds.cookie.clone();
|
||||
authorization_header = creds.authorization_header_value();
|
||||
// TODO(bt,2025-04-23): We're using a Sora payload error in place of application state error. Surface this differently.
|
||||
sentinel = creds.sentinel.as_ref()
|
||||
.map(|sentinel| sentinel.to_string())
|
||||
.ok_or(SoraImageGenError::SentinelBlock("Sentinel is required for image generation.".to_string()))?;
|
||||
}
|
||||
CredentialMigrationRef::New(creds) => {
|
||||
cookie = creds.cookies.to_string();
|
||||
// TODO(bt,2025-04-23): We're using a Sora payload error in place of application state error. Surface this differently.
|
||||
authorization_header = creds.jwt_bearer_token.as_ref()
|
||||
.ok_or(SoraImageGenError::InvalidJwt("JWT bearer is required for image generation".to_string()))?
|
||||
.to_authorization_header_value();
|
||||
// TODO(bt,2025-04-23): We're using a Sora payload error in place of application state error. Surface this differently.
|
||||
sentinel = creds.sora_sentinel.as_ref()
|
||||
.map(|sentinel| sentinel.get_sentinel().to_string())
|
||||
.ok_or(SoraImageGenError::SentinelBlock("Sentinel is required for image generation.".to_string()))?;
|
||||
}
|
||||
}
|
||||
let authorization_header = credentials.jwt_bearer_token.as_ref()
|
||||
.ok_or(SoraClientError::NoBearerTokenForRequest)?
|
||||
.to_authorization_header_value();
|
||||
|
||||
let sentinel = credentials.sora_sentinel.as_ref()
|
||||
.map(|sentinel| sentinel.get_sentinel().to_string())
|
||||
.ok_or(SoraClientError::NoSentinelTokenForRequest)?;
|
||||
|
||||
let mut http_request = client.post(SORA_IMAGE_GEN_URL)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
@@ -199,52 +162,70 @@ pub (crate) async fn image_gen_http_request(
|
||||
}
|
||||
|
||||
let http_request = http_request.json(&sora_request).build()
|
||||
.map_err(|e| SoraImageGenError::NetworkError(e.to_string()))?;
|
||||
.map_err(|err| {
|
||||
error!("Error building Sora image generation HTTP request: {:?}", err);
|
||||
SoraClientError::WreqClientError(err)
|
||||
})?;
|
||||
|
||||
let response = client.execute(http_request)
|
||||
.await
|
||||
.map_err(|e| SoraImageGenError::NetworkError(e.to_string()))?;
|
||||
.map_err(|err| {
|
||||
error!("Error during Sora image generation request: {:?}", err);
|
||||
SoraClientError::WreqClientError(err)
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_body = &response.text().await
|
||||
.map_err(|e| SoraImageGenError::NetworkError(e.to_string()))?;
|
||||
.map_err(|err| {
|
||||
error!("Error reading Sora image generation response body: {:?}", err);
|
||||
SoraClientError::WreqClientError(err)
|
||||
})?;
|
||||
|
||||
if status != reqwest::StatusCode::OK {
|
||||
if status != StatusCode::OK {
|
||||
warn!("Sora image generation failure. Raw response: {:?}", response_body);
|
||||
|
||||
let error_response: RawSoraErrorResponse = serde_json::from_str(response_body)
|
||||
.map_err(|e| SoraImageGenError::GenericError(format!("Failed to parse error response: {}", e)))?;
|
||||
.map_err(|err| SoraGenericApiError::SerdeParseErrorWithBodyOnNon200(err, response_body.to_string()))?;
|
||||
|
||||
// Check for specific error codes
|
||||
if let Some(code) = &error_response.error.code {
|
||||
match code {
|
||||
SoraErrorCode::TokenExpired => {
|
||||
return Err(SoraImageGenError::TokenExpired(error_response.error.message));
|
||||
error!("Sora token expired: {}", error_response.error);
|
||||
return Err(SoraSpecificApiError::TokenExpiredError.into());
|
||||
}
|
||||
SoraErrorCode::SentinelBlock => {
|
||||
return Err(SoraImageGenError::SentinelBlock(error_response.error.message));
|
||||
error!("Sora image generation error sentinel block: {}", error_response.error);
|
||||
return Err(SoraSpecificApiError::SentinelBlockError.into());
|
||||
}
|
||||
SoraErrorCode::TooManyConcurrentTasks => {
|
||||
return Err(SoraImageGenError::TooManyConcurrentTasks(error_response.error.message));
|
||||
error!("Too many concurrent tasks. Please wait.");
|
||||
return Err(SoraSpecificApiError::TooManyConcurrentTasks.into());
|
||||
}
|
||||
SoraErrorCode::Unknown(_) => {
|
||||
return Err(SoraImageGenError::GenericError(error_response.error.message));
|
||||
SoraErrorCode::Unknown(message) => {
|
||||
error!("Unknown error: {} - message: {}", error_response.error.message, message);
|
||||
return Err(SoraGenericApiError::UncategorizedBadResponse(error_response.error.message).into());
|
||||
}
|
||||
SoraErrorCode::InvalidJwt => {
|
||||
return Err(SoraImageGenError::InvalidJwt(error_response.error.message));
|
||||
error!("Invalid JWT token: {}", error_response.error.message);
|
||||
return Err(SoraSpecificApiError::InvalidJwt.into());
|
||||
}
|
||||
SoraErrorCode::UsernameRequired => {
|
||||
return Err(SoraImageGenError::UsernameRequired(error_response.error.message));
|
||||
error!("Username required: {}", error_response.error.message);
|
||||
return Err(SoraSpecificApiError::SoraUsernameNotYetCreated.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Err(SoraImageGenError::GenericError(error_response.error.message));
|
||||
error!("Sora image generation failure: {}", error_response.error);
|
||||
return Err(SoraGenericApiError::UncategorizedBadResponse(
|
||||
format!("Unknown error code: {:?}, message: {:?}", error_response.error.code, error_response.error.message))
|
||||
.into());
|
||||
}
|
||||
|
||||
let response = serde_json::from_str(response_body)
|
||||
.map_err(|e| SoraImageGenError::GenericError(format!("Failed to parse success response: {}", e)))?;
|
||||
.map_err(|err| SoraGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
pub (crate) mod image_gen_http_request;
|
||||
pub use image_gen_http_request::SoraImageGenError;
|
||||
pub mod common;
|
||||
pub mod sora_image_gen_remix;
|
||||
pub mod sora_image_gen_simple;
|
||||
pub mod image_gen_status;
|
||||
|
||||
+12
-12
@@ -1,6 +1,7 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
|
||||
use crate::requests::image_gen::image_gen_http_request::{image_gen_http_request, InpaintItem, InpaintItemType, OperationType, RawSoraImageGenRequest, SoraImageGenError, VideoGenType};
|
||||
use crate::requests::image_gen::image_gen_http_request::{image_gen_http_request, InpaintItem, InpaintItemType, OperationType, RawSoraImageGenRequest, VideoGenType};
|
||||
use log::info;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -9,13 +10,13 @@ pub struct SoraImageGenRemixRequest<'a> {
|
||||
pub num_images: NumImages,
|
||||
pub image_size: ImageSize,
|
||||
pub sora_media_tokens: Vec<String>,
|
||||
pub credentials: CredentialMigrationRef<'a>,
|
||||
pub credentials: &'a SoraCredentialSet,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
/// The "remix" commands let you supply additional images as context.
|
||||
/// Sora "media tokens" of previously uploaded images must be supplied.
|
||||
pub async fn sora_image_gen_remix(request: SoraImageGenRemixRequest<'_>) -> Result<SoraImageGenResponse, SoraImageGenError> {
|
||||
pub async fn sora_image_gen_remix(request: SoraImageGenRemixRequest<'_>) -> Result<SoraImageGenResponse, SoraError> {
|
||||
let args = RawSoraImageGenRequest {
|
||||
r#type: VideoGenType::ImageGen,
|
||||
operation: OperationType::Remix,
|
||||
@@ -50,8 +51,7 @@ pub async fn sora_image_gen_remix(request: SoraImageGenRemixRequest<'_>) -> Resu
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_builder::SoraCredentialBuilder;
|
||||
use crate::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use crate::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
|
||||
use errors::AnyhowResult;
|
||||
@@ -70,18 +70,18 @@ mod tests {
|
||||
let bearer = read_to_string(test_file_path("test_data/temp/bearer.txt")?)?;
|
||||
let bearer = bearer.trim().to_string();
|
||||
|
||||
let creds = SoraCredentials {
|
||||
bearer_token: bearer,
|
||||
cookie,
|
||||
sentinel: Some(sentinel),
|
||||
};
|
||||
let creds = SoraCredentialBuilder::new()
|
||||
.with_cookies(&cookie)
|
||||
.with_jwt_bearer_token(&bearer)
|
||||
.with_sora_sentinel(&sentinel)
|
||||
.build()?;
|
||||
|
||||
let response = sora_image_gen_remix(SoraImageGenRemixRequest {
|
||||
prompt: "Match the pose and scene layout from the uploaded image exactly. A smart dog getting kisses from another dog. Brick building. Anime style".to_string(),
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
sora_media_tokens: vec!["media_01jqyhrz4detyvtzwp2p4j63ad".to_string()],
|
||||
credentials: CredentialMigrationRef::Legacy(&creds),
|
||||
credentials: &creds,
|
||||
request_timeout: None,
|
||||
}).await?;
|
||||
|
||||
|
||||
+16
-14
@@ -1,18 +1,18 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
|
||||
use crate::requests::image_gen::image_gen_http_request::{image_gen_http_request, OperationType, RawSoraImageGenRequest, VideoGenType};
|
||||
use crate::requests::image_gen::SoraImageGenError;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct SoraImageGenSimpleRequest<'a> {
|
||||
pub prompt: String,
|
||||
pub num_images: NumImages,
|
||||
pub image_size: ImageSize,
|
||||
pub credentials: CredentialMigrationRef<'a>,
|
||||
pub credentials: &'a SoraCredentialSet,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub async fn sora_image_gen_simple(args: SoraImageGenSimpleRequest<'_>) -> Result<SoraImageGenResponse, SoraImageGenError> {
|
||||
pub async fn sora_image_gen_simple(args: SoraImageGenSimpleRequest<'_>) -> Result<SoraImageGenResponse, SoraError> {
|
||||
let sora_request = RawSoraImageGenRequest {
|
||||
r#type: VideoGenType::ImageGen,
|
||||
operation: OperationType::SimpleCompose,
|
||||
@@ -24,8 +24,11 @@ pub async fn sora_image_gen_simple(args: SoraImageGenSimpleRequest<'_>) -> Resul
|
||||
inpaint_items: vec![],
|
||||
};
|
||||
|
||||
// TODO: Error handling.
|
||||
let result = image_gen_http_request(sora_request, args.credentials, args.request_timeout).await?;
|
||||
let result = image_gen_http_request(
|
||||
sora_request,
|
||||
args.credentials,
|
||||
args.request_timeout
|
||||
).await?;
|
||||
|
||||
Ok(SoraImageGenResponse {
|
||||
task_id: result.id,
|
||||
@@ -34,8 +37,7 @@ pub async fn sora_image_gen_simple(args: SoraImageGenSimpleRequest<'_>) -> Resul
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_builder::SoraCredentialBuilder;
|
||||
use crate::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use crate::requests::image_gen::sora_image_gen_simple::{sora_image_gen_simple, SoraImageGenSimpleRequest};
|
||||
use errors::AnyhowResult;
|
||||
@@ -54,17 +56,17 @@ mod tests {
|
||||
let bearer = read_to_string(test_file_path("test_data/temp/bearer.txt")?)?;
|
||||
let bearer = bearer.trim().to_string();
|
||||
|
||||
let creds = SoraCredentials {
|
||||
bearer_token: bearer,
|
||||
cookie,
|
||||
sentinel: Some(sentinel),
|
||||
};
|
||||
let creds = SoraCredentialBuilder::new()
|
||||
.with_cookies(&cookie)
|
||||
.with_jwt_bearer_token(&bearer)
|
||||
.with_sora_sentinel(&sentinel)
|
||||
.build()?;
|
||||
|
||||
let response = sora_image_gen_simple(SoraImageGenSimpleRequest {
|
||||
prompt: "A pirate and a ninja fight in a battle inside a UFO. Fully photo realistic, lifelike, lens flare".to_string(),
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
credentials: CredentialMigrationRef::Legacy(&creds),
|
||||
credentials: &creds,
|
||||
request_timeout: None,
|
||||
}).await?;
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod sora_job_status;
|
||||
@@ -1,81 +0,0 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::sora_error::SoraError;
|
||||
|
||||
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";
|
||||
|
||||
// https://sora.com/backend/notif?limit=100&before=task_01jqwwrkvgeqp8jsf5mqk1jceh
|
||||
|
||||
pub async fn sora_job_status(
|
||||
task_id: &str,
|
||||
credentials: CredentialMigrationRef<'_>,
|
||||
) -> Result<(), SoraError> {
|
||||
let url = format!("https://sora.com/backend/notif?limit=100&before={}", task_id);
|
||||
|
||||
let mut cookie;
|
||||
let mut authorization_header;
|
||||
|
||||
match credentials {
|
||||
CredentialMigrationRef::Legacy(creds) => {
|
||||
cookie = creds.cookie.clone();
|
||||
authorization_header = creds.authorization_header_value();
|
||||
}
|
||||
CredentialMigrationRef::New(creds) => {
|
||||
cookie = creds.cookies.to_string();
|
||||
// TODO(bt,2025-04-23): We're using a Sora payload error in place of application state error. Surface this differently.
|
||||
authorization_header = creds.jwt_bearer_token.as_ref()
|
||||
.ok_or(SoraError::NoBearerTokenAvailable)?
|
||||
.to_authorization_header_value();
|
||||
}
|
||||
}
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let request = client
|
||||
.get(url)
|
||||
.header("Authorization", &authorization_header)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Cookie", &cookie);
|
||||
|
||||
let response = request.send().await?;
|
||||
|
||||
let json_response = &response.text().await?;
|
||||
|
||||
//let response = serde_json::from_str(json_response)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::requests::job::sora_job_status::sora_job_status;
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
pub async fn manual_test() -> AnyhowResult<()> {
|
||||
let sentinel = read_to_string(test_file_path("test_data/temp/sentinel.txt")?)?;
|
||||
let sentinel = sentinel.trim().to_string();
|
||||
|
||||
let cookie = read_to_string(test_file_path("test_data/temp/cookie.txt")?)?;
|
||||
let cookie = cookie.trim().to_string();
|
||||
|
||||
let bearer = read_to_string(test_file_path("test_data/temp/bearer.txt")?)?;
|
||||
let bearer = bearer.trim().to_string();
|
||||
|
||||
let creds = SoraCredentials {
|
||||
bearer_token: bearer,
|
||||
cookie,
|
||||
sentinel: Some(sentinel),
|
||||
};
|
||||
|
||||
let task_id = "task_01jqwwrkvgeqp8jsf5mqk1jceh";
|
||||
|
||||
let result = sora_job_status(task_id, CredentialMigrationRef::Legacy(&creds)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod sora_job_status;
|
||||
pub mod unimplemented_sora_job_status;
|
||||
+47
-37
@@ -1,30 +1,22 @@
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::sora_error::SoraError;
|
||||
use crate::constants::user_agent::USER_AGENT;
|
||||
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_generic_api_error::SoraGenericApiError;
|
||||
use crate::requests::common::task_id::TaskId;
|
||||
use crate::utils_internal::classify_general_http_status_code_and_body::classify_general_http_status_code_and_body;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use reqwest::Url;
|
||||
use log::{debug, error, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
use log::{debug, error, warn};
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use crate::utils::classify_general_http_status_code_and_body::classify_general_http_status_code_and_body;
|
||||
use url::Url;
|
||||
use wreq::Client;
|
||||
|
||||
const SORA_STATUS_URL: &str = "https://sora.com/backend/video_gen";
|
||||
|
||||
/// 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";
|
||||
|
||||
/// A strongly typed task ID for Sora tasks.
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
|
||||
pub struct TaskId(pub String);
|
||||
|
||||
impl Display for TaskId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub enum TaskStatus {
|
||||
@@ -207,15 +199,17 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
|
||||
let bearer_header = match credentials.jwt_bearer_token.as_ref() {
|
||||
Some(bearer) => bearer.to_authorization_header_value(),
|
||||
None => {
|
||||
warn!("No JWT bearer for getting image gen status!");
|
||||
return Err(SoraError::NoBearerTokenAvailable)
|
||||
warn!("No JWT bearer token in client - cannot fetch image gen status!");
|
||||
return Err(SoraClientError::NoBearerTokenForRequest.into());
|
||||
},
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let client = Client::new();
|
||||
|
||||
let mut url = reqwest::Url::parse(SORA_STATUS_URL)
|
||||
.map_err(|err| anyhow!("error parsing URL: {:?}", err))?;
|
||||
let mut url = Url::parse(SORA_STATUS_URL)
|
||||
.map_err(|err| {
|
||||
SoraClientError::UrlParseError(err)
|
||||
})?;
|
||||
|
||||
// Add query parameters
|
||||
if let Some(limit) = status_request.limit {
|
||||
@@ -226,19 +220,32 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
|
||||
url.query_pairs_mut().append_pair("before", &before);
|
||||
}
|
||||
|
||||
let http_request = client.get(url)
|
||||
let http_request = client.get(url.as_str())
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Cookie", credentials.cookies.as_str())
|
||||
.header("Authorization", bearer_header)
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
let http_request = http_request.build()?;
|
||||
let http_request = http_request.build()
|
||||
.map_err(|err| {
|
||||
SoraClientError::WreqClientError(err)
|
||||
})?;
|
||||
|
||||
let response = client.execute(http_request).await?;
|
||||
let response = client.execute(http_request)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Client failed to fetch sora image gen status: {:?}", err);
|
||||
SoraGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_body = &response.text().await?;
|
||||
let response_body = &response.text()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Client failed to read sora image gen status: {:?}", err);
|
||||
SoraGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
debug!("response_body: {}", response_body);
|
||||
|
||||
@@ -254,7 +261,8 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) => {
|
||||
warn!("Failed to parse status response: {:?}", err);
|
||||
Err(SoraError::JsonErrorWithBody(err, response_body.to_string()))
|
||||
Err(SoraGenericApiError::SerdeResponseParseErrorWithBody(
|
||||
err, response_body.to_string()).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,7 +273,7 @@ pub async fn save_generations_to_dir(generations: &[Generation], dir: &str) -> A
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
for generation in generations {
|
||||
let response = reqwest::get(generation.url.clone()).await?;
|
||||
let response = wreq::get(generation.url.clone()).send().await?;
|
||||
let image_bytes = response.bytes().await?;
|
||||
|
||||
let url = Url::parse(&generation.url)?;
|
||||
@@ -276,7 +284,7 @@ pub async fn save_generations_to_dir(generations: &[Generation], dir: &str) -> A
|
||||
file.write_all(&image_bytes)?;
|
||||
|
||||
let thumbnail_url = generation.encodings.thumbnail.path.clone();
|
||||
let thumbnail_response = reqwest::get(thumbnail_url).await?;
|
||||
let thumbnail_response = wreq::get(thumbnail_url).send().await?;
|
||||
let thumbnail_bytes = thumbnail_response.bytes().await?;
|
||||
let thumbnail_path = Path::new(dir).join(format!("{}_thumbnail.{}", generation.id, ext));
|
||||
let mut thumbnail_file = File::create(thumbnail_path)?;
|
||||
@@ -288,13 +296,12 @@ pub async fn save_generations_to_dir(generations: &[Generation], dir: &str) -> A
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::creds::sora_credential_builder::SoraCredentialBuilder;
|
||||
use crate::recipes::wait_for_image_gen_status::wait_for_image_gen_status;
|
||||
use crate::requests::image_gen::image_gen_status::{get_image_gen_status, save_generations_to_dir, StatusRequest, VideoGenStatusResponse};
|
||||
use crate::requests::job_status::sora_job_status::{get_image_gen_status, save_generations_to_dir, StatusRequest, VideoGenStatusResponse};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
@@ -316,13 +323,16 @@ mod tests {
|
||||
let bearer = read_to_string(test_file_path("test_data/temp/bearer.txt")?)?;
|
||||
let bearer = bearer.trim().to_string();
|
||||
|
||||
let creds = SoraCredentials { bearer_token: bearer, cookie, sentinel: Some(sentinel) };
|
||||
let new_creds = SoraCredentialSet::from_legacy_credentials(&creds)?;
|
||||
let creds = SoraCredentialBuilder::new()
|
||||
.with_cookies(&cookie)
|
||||
.with_jwt_bearer_token(&bearer)
|
||||
.with_sora_sentinel(&sentinel)
|
||||
.build()?;
|
||||
|
||||
// Get the task status for a specific task
|
||||
// let response = get_image_gen_status(&StatusRequest::new(None, Some("task_01jr9yvpfyetx9r7qvvx38scna".to_string())), &creds).await?;
|
||||
|
||||
let response = get_image_gen_status(&StatusRequest::new(Some(50), None), &new_creds).await?;
|
||||
let response = get_image_gen_status(&StatusRequest::new(Some(50), None), &creds).await?;
|
||||
println!("task_id: {}", response.task_responses[0].id);
|
||||
|
||||
let task_id = response.task_responses[0].id.clone();
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
use crate::constants::user_agent::USER_AGENT;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::error::sora_client_error::SoraClientError;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::error::sora_generic_api_error::SoraGenericApiError;
|
||||
use log::error;
|
||||
use wreq::Client;
|
||||
|
||||
// https://sora.com/backend/notif?limit=100&before=task_01jqwwrkvgeqp8jsf5mqk1jceh
|
||||
|
||||
pub async fn unimplemented_sora_job_status(
|
||||
task_id: &str,
|
||||
credentials: &SoraCredentialSet,
|
||||
) -> Result<(), SoraError> {
|
||||
let url = format!("https://sora.com/backend/notif?limit=100&before={}", task_id);
|
||||
|
||||
let cookie = credentials.cookies.to_string();
|
||||
|
||||
let authorization_header = credentials.jwt_bearer_token.as_ref()
|
||||
.ok_or(SoraClientError::NoBearerTokenForRequest)?
|
||||
.to_authorization_header_value();
|
||||
|
||||
let client = Client::new();
|
||||
|
||||
let request = client
|
||||
.get(url)
|
||||
.header("Authorization", &authorization_header)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Cookie", &cookie);
|
||||
|
||||
let response = request.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("sora fetch job status error: {}", err);
|
||||
SoraGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let json_response = &response.text().await
|
||||
.map_err(|err| {
|
||||
error!("sora read job status error: {}", err);
|
||||
SoraGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
unimplemented!("job status is not parsed")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::sora_credential_builder::SoraCredentialBuilder;
|
||||
use crate::requests::job_status::unimplemented_sora_job_status::unimplemented_sora_job_status;
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
pub async fn manual_test() -> AnyhowResult<()> {
|
||||
let sentinel = read_to_string(test_file_path("test_data/temp/sentinel.txt")?)?;
|
||||
let sentinel = sentinel.trim().to_string();
|
||||
|
||||
let cookie = read_to_string(test_file_path("test_data/temp/cookie.txt")?)?;
|
||||
let cookie = cookie.trim().to_string();
|
||||
|
||||
let bearer = read_to_string(test_file_path("test_data/temp/bearer.txt")?)?;
|
||||
let bearer = bearer.trim().to_string();
|
||||
|
||||
let creds = SoraCredentialBuilder::new()
|
||||
.with_cookies(&cookie)
|
||||
.with_jwt_bearer_token(&bearer)
|
||||
.with_sora_sentinel(&sentinel)
|
||||
.build()?;
|
||||
|
||||
let task_id = "task_01jqwwrkvgeqp8jsf5mqk1jceh";
|
||||
|
||||
let result = unimplemented_sora_job_status(task_id, &creds).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::error::sora_client_error::SoraClientError;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::error::sora_generic_api_error::SoraGenericApiError;
|
||||
use crate::requests::upload::upload_media_http_request::SoraMediaUploadResponse;
|
||||
use crate::sora_error::SoraError;
|
||||
use crate::utils::classify_general_http_error::classify_general_http_error;
|
||||
use crate::utils_internal::classify_general_http_error::classify_general_http_error;
|
||||
use log::{error, info};
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::{Client, Url};
|
||||
use serde_derive::Deserialize;
|
||||
use wreq::Client;
|
||||
|
||||
const SORA_MEDIA_LIST_URL: &str = "https://sora.chatgpt.com/backend/video_gen?limit=50";
|
||||
|
||||
@@ -39,13 +41,17 @@ pub async fn list_media(credentials: &SoraCredentialSet) -> Result<ListMediaResp
|
||||
let auth_header = credentials.jwt_bearer_token
|
||||
.as_ref()
|
||||
.map(|bearer| bearer.to_authorization_header_value())
|
||||
.ok_or_else(|| SoraError::NoBearerTokenAvailable)?;
|
||||
.ok_or_else(|| SoraClientError::NoBearerTokenForRequest)?;
|
||||
|
||||
let cookie = credentials.cookies.to_string();
|
||||
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()?;
|
||||
.build()
|
||||
.map_err(|err| {
|
||||
error!("Failed to build HTTP client: {}", err);
|
||||
SoraGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let response = client.get(SORA_MEDIA_LIST_URL)
|
||||
.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0")
|
||||
@@ -55,7 +61,11 @@ pub async fn list_media(credentials: &SoraCredentialSet) -> Result<ListMediaResp
|
||||
.header("Cookie", &cookie)
|
||||
.header("Authorization", &auth_header)
|
||||
.send()
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to fetch media list: {}", err);
|
||||
SoraGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
error!("Failed to fetch media list: {}", response.status());
|
||||
@@ -65,9 +75,19 @@ pub async fn list_media(credentials: &SoraCredentialSet) -> Result<ListMediaResp
|
||||
|
||||
info!("Successfully generated bearer token.");
|
||||
|
||||
// Parse response
|
||||
let typed_response = response.json::<ListMediaResponse>().await?;
|
||||
Ok(typed_response)
|
||||
let text_body = &response.text().await
|
||||
.map_err(|err| {
|
||||
error!("sora error reading media list text body: {}", err);
|
||||
SoraGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let response = serde_json::from_str::<ListMediaResponse>(text_body)
|
||||
.map_err(|err| {
|
||||
error!("Failed to parse media list response: {}", err);
|
||||
SoraGenericApiError::SerdeResponseParseErrorWithBody(err, text_body.to_string())
|
||||
})?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod bearer;
|
||||
pub mod auth_bearer;
|
||||
pub mod auth_sentinel;
|
||||
pub mod common;
|
||||
pub mod image_gen;
|
||||
pub mod job;
|
||||
pub mod job_status;
|
||||
pub mod list_media;
|
||||
pub mod sentinel_refresh;
|
||||
pub mod upload;
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod request;
|
||||
pub mod token;
|
||||
pub(crate) mod config;
|
||||
@@ -1 +0,0 @@
|
||||
pub mod generate;
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::requests::upload::upload_media_http_request::{upload_media_http_request, SoraMediaUploadResponse};
|
||||
use crate::sora_error::SoraError;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::time::Duration;
|
||||
pub async fn sora_media_upload_from_bytes(
|
||||
bytes: Vec<u8>,
|
||||
file_name: String,
|
||||
creds: CredentialMigrationRef<'_>,
|
||||
creds: &SoraCredentialSet,
|
||||
maybe_timeout: Option<Duration>,
|
||||
) -> Result<SoraMediaUploadResponse, SoraError> {
|
||||
let file_path = PathBuf::from(&file_name);
|
||||
|
||||
+27
-15
@@ -1,6 +1,8 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::error::sora_client_error::SoraClientError;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::requests::upload::upload_media_http_request::{upload_media_http_request, SoraMediaUploadResponse};
|
||||
use crate::sora_error::SoraError;
|
||||
use log::error;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::fs::File;
|
||||
@@ -12,20 +14,32 @@ const INITIAL_BUFFER_SIZE : usize = 1024*1024;
|
||||
|
||||
pub async fn sora_media_upload_from_file<P: AsRef<Path>>(
|
||||
file_path: P,
|
||||
creds: CredentialMigrationRef<'_>,
|
||||
creds: &SoraCredentialSet,
|
||||
maybe_timeout: Option<Duration>,
|
||||
) -> Result<SoraMediaUploadResponse, SoraError> {
|
||||
let mut file = File::open(&file_path).await?;
|
||||
let mut file = File::open(&file_path).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to open file for upload: {}", err);
|
||||
SoraClientError::FileForUploadReadError(err)
|
||||
})?;
|
||||
|
||||
let mut buffer = Vec::with_capacity(INITIAL_BUFFER_SIZE);
|
||||
file.read_to_end(&mut buffer).await?;
|
||||
|
||||
file.read_to_end(&mut buffer).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to read file for upload: {}", err);
|
||||
SoraClientError::FileForUploadReadError(err)
|
||||
})?;
|
||||
|
||||
let filename = file_path.as_ref().file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid file path"))?
|
||||
.ok_or_else(|| SoraClientError::FileForUploadHasInvalidPath)?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let maybe_ext = file_path.as_ref().extension().and_then(|e| e.to_str());
|
||||
|
||||
// TODO: Read file magic bytes first, then fall back to this.
|
||||
let mime_type = match file_path.as_ref().extension().and_then(|e| e.to_str()) {
|
||||
let mime_type = match maybe_ext {
|
||||
Some("jpg") | Some("jpeg") => "image/jpeg",
|
||||
Some("png") => "image/png",
|
||||
// Some("webp") => "image/webp",
|
||||
@@ -41,8 +55,7 @@ pub async fn sora_media_upload_from_file<P: AsRef<Path>>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_builder::SoraCredentialBuilder;
|
||||
use crate::requests::upload::upload_media_from_file::sora_media_upload_from_file;
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
@@ -59,15 +72,14 @@ mod tests {
|
||||
|
||||
let image_path = test_file_path("test_data/image/juno.jpg")?; // media_01jqyqgqpwf40tkcapq5bmaz5d
|
||||
|
||||
let creds = SoraCredentials {
|
||||
bearer_token: bearer,
|
||||
cookie,
|
||||
sentinel: None,
|
||||
};
|
||||
let creds = SoraCredentialBuilder::new()
|
||||
.with_cookies(&cookie)
|
||||
.with_jwt_bearer_token(&bearer)
|
||||
.build()?;
|
||||
|
||||
let response = sora_media_upload_from_file(
|
||||
image_path,
|
||||
CredentialMigrationRef::Legacy(&creds),
|
||||
&creds,
|
||||
None,
|
||||
).await?;
|
||||
|
||||
|
||||
+39
-32
@@ -1,12 +1,15 @@
|
||||
use std::time::Duration;
|
||||
use crate::credentials::{SoraCredentials, USER_AGENT};
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::sora_error::SoraError;
|
||||
use crate::utils::classify_general_http_error::classify_general_http_error;
|
||||
use log::info;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use reqwest::Client;
|
||||
use crate::constants::user_agent::USER_AGENT;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::error::sora_client_error::SoraClientError;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::error::sora_generic_api_error::SoraGenericApiError;
|
||||
use crate::utils_internal::classify_general_http_error::classify_general_http_error;
|
||||
use log::{error, info};
|
||||
use serde::Deserialize;
|
||||
use std::io::empty;
|
||||
use std::time::Duration;
|
||||
use wreq::multipart::{Form, Part};
|
||||
use wreq::Client;
|
||||
|
||||
const SORA_UPLOAD_MEDIA_URL: &str = "https://sora.com/backend/uploads";
|
||||
|
||||
@@ -62,40 +65,30 @@ pub struct SoraMediaUploadResponse {
|
||||
|
||||
pub (crate) struct SoraMediaUploadRequest<'a> {
|
||||
pub file_path: String,
|
||||
pub credentials: &'a SoraCredentials,
|
||||
pub credentials: &'a SoraCredentialSet,
|
||||
}
|
||||
|
||||
pub (crate) async fn upload_media_http_request(
|
||||
file_bytes: Vec<u8>,
|
||||
filename: String,
|
||||
mime_type: &str,
|
||||
credentials: CredentialMigrationRef<'_>,
|
||||
credentials: &SoraCredentialSet,
|
||||
maybe_timeout: Option<Duration>,
|
||||
) -> Result<SoraMediaUploadResponse, SoraError> {
|
||||
|
||||
// Create multipart form
|
||||
let part = Part::bytes(file_bytes) // NB: Reqwest needs to own the bytes.
|
||||
.file_name(filename) // NB: Reqwest needs to own the bytes
|
||||
.mime_str(mime_type)?;
|
||||
let part = Part::bytes(file_bytes) // NB: Reqwest needs to own the bytes.
|
||||
.file_name(filename) // NB: Reqwest needs to own the bytes
|
||||
.mime_str(mime_type)
|
||||
.map_err(|e| SoraClientError::MultipartFormError(e))?;
|
||||
|
||||
let form = Form::new().part("file", part);
|
||||
|
||||
let cookie;
|
||||
let auth_header;
|
||||
|
||||
match credentials {
|
||||
CredentialMigrationRef::Legacy(creds) => {
|
||||
cookie = creds.cookie.clone();
|
||||
auth_header = creds.authorization_header_value();
|
||||
}
|
||||
CredentialMigrationRef::New(creds) => {
|
||||
cookie = creds.cookies.to_string();
|
||||
auth_header = creds.jwt_bearer_token
|
||||
.as_ref()
|
||||
.ok_or(SoraError::NoBearerTokenAvailable)?
|
||||
.to_authorization_header_value();
|
||||
}
|
||||
}
|
||||
let cookie = credentials.cookies.to_string();
|
||||
let auth_header = credentials.jwt_bearer_token
|
||||
.as_ref()
|
||||
.ok_or(SoraClientError::NoBearerTokenForRequest)?
|
||||
.to_authorization_header_value();
|
||||
|
||||
// Make API request
|
||||
let client = Client::new();
|
||||
@@ -109,7 +102,11 @@ pub (crate) async fn upload_media_http_request(
|
||||
request_builder = request_builder.timeout(timeout);
|
||||
}
|
||||
|
||||
let response = request_builder.send().await?;
|
||||
let response = request_builder.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
SoraGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
// Check response status
|
||||
if !response.status().is_success() {
|
||||
@@ -118,7 +115,17 @@ pub (crate) async fn upload_media_http_request(
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
// Parse response
|
||||
let upload_response = response.json::<SoraMediaUploadResponse>().await?;
|
||||
let response_body = &response.text().await
|
||||
.map_err(|err| {
|
||||
error!("Error reading response body while attempting file upload: {:?}", err);
|
||||
SoraGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let upload_response : SoraMediaUploadResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| {
|
||||
error!("Error parsing response body while attempting file upload: {:?}", err);
|
||||
SoraGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string())
|
||||
})?;
|
||||
|
||||
Ok(upload_response)
|
||||
}
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
|
||||
/// The single error type this crate should surface to all callers.
|
||||
#[derive(Debug)]
|
||||
pub enum SoraError {
|
||||
/// We haven't received a bearer token yet
|
||||
/// This is our own internal application state error, not something Sora returns.
|
||||
NoBearerTokenAvailable,
|
||||
|
||||
/// We're sending too many tasks to Sora.
|
||||
TooManyConcurrentTasks,
|
||||
|
||||
// TODO: Try to make concrete error types for these.
|
||||
/// 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,
|
||||
|
||||
/// Failed to generate a bearer token despite the `200 OK` response.
|
||||
/// This has been generated by sending old/expired session cookies or empty cookies.
|
||||
FailedToGenerateBearer,
|
||||
|
||||
/// Sora requires users to set up an account username before they can generate images.
|
||||
/// This requires the user to set up that username.
|
||||
SoraUsernameNotYetCreated,
|
||||
|
||||
/// Something is wrong with the JWT bearer token.
|
||||
/// This error originates on our end as we try to parse the JWT.
|
||||
LocalJwtClaimsParseError(String),
|
||||
|
||||
/// Sora is having issues.
|
||||
/// Typically served from Cloudflare
|
||||
/// We preserve the message for debugging.
|
||||
BadGateway(String),
|
||||
|
||||
/// Specific Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
/// Another error occurred.
|
||||
OtherBadStatus(anyhow::Error),
|
||||
|
||||
/// Reqwest Error, eg. connection failures.
|
||||
ReqwestError(reqwest::Error),
|
||||
|
||||
/// std::io Error that arises from our end, eg. reading from the filesystem.
|
||||
IoError(io::Error),
|
||||
|
||||
/// anyhow::Error arises from our end
|
||||
AnyhowError(anyhow::Error),
|
||||
|
||||
/// serde_json::Error, likely from JSON deserialization schema mismatch.
|
||||
JsonError(serde_json::Error),
|
||||
|
||||
/// serde_json::Error, likely from JSON deserialization schema mismatch.
|
||||
/// Includes the original body.
|
||||
JsonErrorWithBody(serde_json::Error, String),
|
||||
}
|
||||
|
||||
impl SoraError {
|
||||
pub fn is_sora_having_downtime_issues(&self) -> bool {
|
||||
match self {
|
||||
Self::BadGateway(_) => true,
|
||||
Self::CloudflareError(CloudflareError::GatewayTimeout504) => true,
|
||||
Self::CloudflareError(CloudflareError::TimeoutOccurred524) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for SoraError {}
|
||||
|
||||
impl Display for SoraError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoBearerTokenAvailable => {
|
||||
write!(f, "Unauthorized: no bearer token set")
|
||||
}
|
||||
Self::FailedToGenerateBearer => {
|
||||
write!(f, "Failed to generate bearer. Probably invalid session.")
|
||||
}
|
||||
Self::SoraUsernameNotYetCreated => {
|
||||
write!(f, "Sora username not created. Please set up a username.")
|
||||
}
|
||||
Self::LocalJwtClaimsParseError(message) => {
|
||||
write!(f, "Local JWT claims parse error: {}", message)
|
||||
}
|
||||
Self::TooManyConcurrentTasks => {
|
||||
write!(f, "Too many concurrent tasks. Please wait.")
|
||||
}
|
||||
Self::UnauthorizedCookieOrBearerExpired => {
|
||||
write!(f, "Unauthorized: cookie and/or bearer token expired")
|
||||
}
|
||||
Self::BadGateway(message) => {
|
||||
write!(f, "Bad Gateway; Sora is likely having issues: {:?}", message)
|
||||
}
|
||||
Self::CloudflareError(error) => {
|
||||
write!(f, "Cloudflare error: {}", error)
|
||||
}
|
||||
Self::OtherBadStatus(err) => {
|
||||
write!(f, "Other error: {}", err)
|
||||
}
|
||||
Self::ReqwestError(err) => {
|
||||
write!(f, "Reqwest error: {}", err)
|
||||
}
|
||||
Self::IoError(err) => {
|
||||
write!(f, "IO error: {}", err)
|
||||
}
|
||||
Self::AnyhowError(err) => {
|
||||
write!(f, "Anyhow error: {}", err)
|
||||
}
|
||||
Self::JsonError(err) => {
|
||||
write!(f, "serde_json error: {}", err)
|
||||
}
|
||||
Self::JsonErrorWithBody(err, message) => {
|
||||
write!(f, "serde_json error: {:?} body: {:?}", err, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for SoraError {
|
||||
fn from(err: reqwest::Error) -> SoraError {
|
||||
Self::ReqwestError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for SoraError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
Self::IoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for SoraError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
Self::AnyhowError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for SoraError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
Self::JsonError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CloudflareError> for SoraError {
|
||||
fn from(err: CloudflareError) -> Self {
|
||||
Self::CloudflareError(err)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
use crate::sora_error::SoraError;
|
||||
use crate::utils::classify_general_http_status_code_and_body::classify_general_http_status_code_and_body;
|
||||
|
||||
/// This assumes the request failed and returned a non-200.
|
||||
/// The caller should check.
|
||||
pub async fn classify_general_http_error(response: reqwest::Response) -> SoraError {
|
||||
let status = response.status();
|
||||
let message = match response.text().await {
|
||||
Ok(text) => text,
|
||||
Err(err) => return SoraError::ReqwestError(err),
|
||||
};
|
||||
|
||||
classify_general_http_status_code_and_body(status, &message).await
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
use std::iter::Map;
|
||||
use cookie::Cookie;
|
||||
use once_cell::sync::Lazy;
|
||||
use errors::AnyhowResult;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub const SESSION_COOKIE_NAME_OLD : &str = "__Secure-authjs.session-token";
|
||||
pub const SESSION_COOKIE_NAME_NEW : &str = "__Secure-next-auth.session-token";
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
pub mod classify_general_http_error;
|
||||
pub mod classify_general_http_status_code_and_body;
|
||||
pub mod has_session_cookie;
|
||||
pub mod lightweight_sora_jwt_parse;
|
||||
pub mod has_session_cookie;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::error::sora_generic_api_error::SoraGenericApiError;
|
||||
use crate::utils_internal::classify_general_http_status_code_and_body::classify_general_http_status_code_and_body;
|
||||
|
||||
/// This assumes the request failed and returned a non-200.
|
||||
/// The caller should check.
|
||||
pub async fn classify_general_http_error(response: wreq::Response) -> SoraError {
|
||||
let status = response.status();
|
||||
let message = match response.text().await {
|
||||
Ok(text) => text,
|
||||
Err(err) => return SoraGenericApiError::WreqError(err).into(),
|
||||
};
|
||||
|
||||
classify_general_http_status_code_and_body(status, &message).await
|
||||
}
|
||||
+12
-7
@@ -1,6 +1,8 @@
|
||||
use crate::sora_error::SoraError;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use crate::error::sora_generic_api_error::SoraGenericApiError;
|
||||
use crate::error::sora_specific_api_error::SoraSpecificApiError;
|
||||
use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors;
|
||||
use reqwest::StatusCode;
|
||||
use wreq::StatusCode;
|
||||
/*
|
||||
|
||||
Old cookie -
|
||||
@@ -38,7 +40,7 @@ pub async fn classify_general_http_status_code_and_body(status: StatusCode, resp
|
||||
|| message.contains("token_expired");
|
||||
|
||||
if cookie_expired {
|
||||
return SoraError::UnauthorizedCookieOrBearerExpired;
|
||||
return SoraSpecificApiError::UnauthorizedCookieOrBearerExpired.into();
|
||||
}
|
||||
|
||||
let needs_onboarding =
|
||||
@@ -46,21 +48,24 @@ pub async fn classify_general_http_status_code_and_body(status: StatusCode, resp
|
||||
|| message.contains("onboarding_required");
|
||||
|
||||
if needs_onboarding {
|
||||
return SoraError::SoraUsernameNotYetCreated;
|
||||
return SoraSpecificApiError::SoraUsernameNotYetCreated.into();
|
||||
}
|
||||
|
||||
let status_code = status.as_u16();
|
||||
|
||||
if let Err(err) = filter_cloudflare_errors(status_code, &response_body) {
|
||||
return SoraError::CloudflareError(err);
|
||||
return SoraGenericApiError::CloudflareError(err).into();
|
||||
}
|
||||
|
||||
match status_code {
|
||||
502 => {
|
||||
return SoraError::BadGateway(message);
|
||||
return SoraGenericApiError::Http502ErrorBadGateway(message.to_string()).into();
|
||||
}
|
||||
_ => {}, // Fall-through
|
||||
}
|
||||
|
||||
SoraError::OtherBadStatus(anyhow::anyhow!("Upload failed with status {}: {}", status, message))
|
||||
SoraGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status,
|
||||
body: response_body.to_string(),
|
||||
}.into()
|
||||
}
|
||||
+10
-7
@@ -1,7 +1,7 @@
|
||||
//! Adapted from 'jwt' crate
|
||||
|
||||
use crate::sora_error::SoraError;
|
||||
use anyhow::anyhow;
|
||||
use crate::error::sora_client_error::SoraClientError;
|
||||
use crate::error::sora_error::SoraError;
|
||||
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
|
||||
use base64::Engine;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -24,11 +24,10 @@ pub fn lightweight_sora_jwt_parse(token: &str) -> Result<SoraJwtClaims, SoraErro
|
||||
let [_header_str, claims_str, _signature_str] = split_components(token)?;
|
||||
|
||||
let claims = decode_base64(claims_str)
|
||||
.map_err(|err| SoraError::LocalJwtClaimsParseError(
|
||||
format!("failure to parse claims base64: {:?}", err)))?;
|
||||
.map_err(|err| as_jwt_error(format!("failure to parse claims base64: {:?}", err)))?;
|
||||
|
||||
let claims : serde_json::Map<String, Value> = serde_json::from_str(&claims)
|
||||
.map_err(|err| SoraError::LocalJwtClaimsParseError(
|
||||
.map_err(|err| as_jwt_error(
|
||||
format!("failure to parse claims json: {:?}", err)))?;
|
||||
|
||||
let iat = claims.get("iat")
|
||||
@@ -110,12 +109,16 @@ fn decode_base64(raw: &str) -> AnyhowResult<String> {
|
||||
}
|
||||
|
||||
fn jwt_error(reason: &str) -> SoraError {
|
||||
SoraError::LocalJwtClaimsParseError(reason.to_string())
|
||||
SoraError::Client(SoraClientError::LocalJwtClaimsParseError(reason.to_string()))
|
||||
}
|
||||
|
||||
fn as_jwt_error(reason: String) -> SoraError {
|
||||
SoraError::Client(SoraClientError::LocalJwtClaimsParseError(reason))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::lightweight_sora_jwt_parse::lightweight_sora_jwt_parse;
|
||||
use crate::utils_internal::lightweight_sora_jwt_parse::lightweight_sora_jwt_parse;
|
||||
|
||||
#[test]
|
||||
fn test_sora_jwt_bearer_token() {
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod classify_general_http_error;
|
||||
pub mod classify_general_http_status_code_and_body;
|
||||
pub mod lightweight_sora_jwt_parse;
|
||||
@@ -3,7 +3,7 @@ use base64::DecodeError;
|
||||
use errors::AnyhowError;
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use midjourney_client::error::midjourney_error::MidjourneyError;
|
||||
use openai_sora_client::sora_error::SoraError;
|
||||
use openai_sora_client::error::sora_error::SoraError;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
-1
@@ -32,7 +32,6 @@ use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_rem
|
||||
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::sora_error::SoraError;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
|
||||
use std::time::Duration;
|
||||
|
||||
-1
@@ -35,7 +35,6 @@ use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_rem
|
||||
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::sora_error::SoraError;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
|
||||
use std::time::Duration;
|
||||
|
||||
-1
@@ -34,7 +34,6 @@ use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_rem
|
||||
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::sora_error::SoraError;
|
||||
use std::time::Duration;
|
||||
use storyteller_client::endpoints::media_files::get_media_file::get_media_file;
|
||||
use tauri::AppHandle;
|
||||
|
||||
-1
@@ -37,7 +37,6 @@ use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_rem
|
||||
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::sora_error::SoraError;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use sqlite_tasks::queries::create_task::{create_task, CreateTaskArgs};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -5,7 +5,6 @@ use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
|
||||
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
|
||||
@@ -60,7 +59,7 @@ pub async fn generate_image(
|
||||
let response = sora_media_upload_from_bytes(
|
||||
image_bytes,
|
||||
filename,
|
||||
CredentialMigrationRef::New(&creds),
|
||||
&creds,
|
||||
Some(SORA_IMAGE_UPLOAD_TIMEOUT))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
@@ -76,7 +75,7 @@ pub async fn generate_image(
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
sora_media_tokens: sora_media_tokens.clone(),
|
||||
credentials: CredentialMigrationRef::New(&creds),
|
||||
credentials: &creds,
|
||||
request_timeout: Some(SORA_IMAGE_REMIX_TIMEOUT),
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
|
||||
@@ -14,15 +14,16 @@ use crate::services::sora::state::sora_credential_manager::SoraCredentialManager
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use errors::AnyhowError;
|
||||
use log::{error, info, warn};
|
||||
use openai_sora_client::error::sora_error::SoraError;
|
||||
use openai_sora_client::error::sora_specific_api_error::SoraSpecificApiError;
|
||||
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
|
||||
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::sora_error::SoraError;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use storyteller_client::endpoints::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::error::storyteller_error::StorytellerError;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
@@ -135,12 +136,12 @@ pub async fn sora_image_remix_command(
|
||||
let mut error_message = "An error occurred. Please try again. If it continues, please tell our staff about the problem.";
|
||||
|
||||
match (err) {
|
||||
InnerError::SoraError(SoraError::TooManyConcurrentTasks) => {
|
||||
InnerError::SoraError(SoraError::ApiSpecific(SoraSpecificApiError::TooManyConcurrentTasks)) => {
|
||||
error_type = SoraImageRemixErrorType::TooManyConcurrentTasks;
|
||||
status = CommandErrorStatus::ServerError;
|
||||
error_message = "You have too many Sora image generation tasks running. Please wait a moment.";
|
||||
},
|
||||
InnerError::SoraError(SoraError::SoraUsernameNotYetCreated) => {
|
||||
InnerError::SoraError(SoraError::ApiSpecific(SoraSpecificApiError::SoraUsernameNotYetCreated)) => {
|
||||
error_type = SoraImageRemixErrorType::SoraUsernameNotYetCreated;
|
||||
status = CommandErrorStatus::BadRequest;
|
||||
error_message = "Your Sora username is not yet created. Please visit the Sora.com website to create it first.";
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::core::state::data_dir::app_data_root::AppDataRoot;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use openai_sora_client::requests::bearer::generate_bearer_with_cookie::generate_bearer_with_cookie;
|
||||
use openai_sora_client::requests::auth_bearer::generate_bearer_jwt_with_cookie::generate_bearer_jwt_with_cookie;
|
||||
|
||||
/// Loading the JWT bearer token for the first time.
|
||||
pub async fn initialize_sora_jwt_bearer_token(app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
@@ -20,7 +20,7 @@ pub async fn initialize_sora_jwt_bearer_token(app_data_root: &AppDataRoot) -> An
|
||||
|
||||
info!("Requesting initial JWT bearer token...");
|
||||
|
||||
let bearer = generate_bearer_with_cookie(&cookie).await?;
|
||||
let bearer = generate_bearer_jwt_with_cookie(&cookie).await?;
|
||||
|
||||
std::fs::write(app_data_root.get_sora_bearer_token_file_path(), bearer)?;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use openai_sora_client::creds::sora_cookies::SoraCookies;
|
||||
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
|
||||
use openai_sora_client::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use openai_sora_client::creds::sora_sentinel::SoraSentinel;
|
||||
use openai_sora_client::requests::sentinel_refresh::generate::token::generate_token;
|
||||
use openai_sora_client::requests::auth_sentinel::generate_sentinel_token::generate_sentinel_token;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
@@ -149,7 +149,7 @@ impl SoraCredentialManager {
|
||||
|
||||
info!("Generating token...");
|
||||
|
||||
let sentinel = generate_token()
|
||||
let sentinel = generate_sentinel_token()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to refresh: {:?}", err);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use errors::AnyhowResult;
|
||||
use indexmap::IndexMap;
|
||||
use openai_sora_client::requests::image_gen::image_gen_status::TaskId;
|
||||
use openai_sora_client::requests::common::task_id::TaskId;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
@@ -154,7 +154,7 @@ impl SoraTaskQueue {
|
||||
mod tests {
|
||||
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
|
||||
use errors::AnyhowResult;
|
||||
use openai_sora_client::requests::image_gen::image_gen_status::TaskId;
|
||||
use openai_sora_client::requests::common::task_id::TaskId;
|
||||
|
||||
#[test]
|
||||
fn remove_list() -> AnyhowResult<()> {
|
||||
|
||||
@@ -12,7 +12,8 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use openai_sora_client::recipes::list_sora_task_status_with_session_auto_renew::{list_sora_task_status_with_session_auto_renew, StatusRequestArgs};
|
||||
use openai_sora_client::requests::image_gen::image_gen_status::{Generation, TaskId, TaskStatus};
|
||||
use openai_sora_client::requests::common::task_id::TaskId;
|
||||
use openai_sora_client::requests::job_status::sora_job_status::{Generation, TaskStatus};
|
||||
use reqwest::Url;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
-1
@@ -24,7 +24,6 @@ use midjourney_client::credentials::cookie_store_has_auth_cookies::cookie_store_
|
||||
use midjourney_client::recipes::get_user_info::{get_user_info, GetUserInfoRequest};
|
||||
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::utils::has_session_cookie::{has_session_cookie, SessionCookiePresence};
|
||||
use tauri::{AppHandle, Manager, WebviewWindow};
|
||||
|
||||
pub async fn storyteller_billing_window_thread(
|
||||
|
||||
+11
-9
@@ -1,15 +1,16 @@
|
||||
use crate::sora_redis_credentials::keys::{BEARER_SUBKEY, COOKIE_SUBKEY, SENTINEL_SUBKEY, SORA_SECRET_REDIS_KEY};
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use openai_sora_client::credentials::SoraCredentials;
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
use openai_sora_client::creds::sora_credential_builder::SoraCredentialBuilder;
|
||||
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
|
||||
use r2d2_redis::r2d2::PooledConnection;
|
||||
use r2d2_redis::redis::Commands;
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
use std::collections::HashMap;
|
||||
use crate::sora_redis_credentials::keys::{BEARER_SUBKEY, COOKIE_SUBKEY, SENTINEL_SUBKEY, SORA_SECRET_REDIS_KEY};
|
||||
|
||||
pub fn get_sora_credentials_from_redis(
|
||||
redis: &mut PooledConnection<RedisConnectionManager>
|
||||
) -> AnyhowResult<SoraCredentials> {
|
||||
) -> AnyhowResult<SoraCredentialSet> {
|
||||
|
||||
let values : HashMap<String, String> = redis.hgetall(SORA_SECRET_REDIS_KEY)
|
||||
.map_err(|e| anyhow!("Failed to get Sora credentials from Redis: {}", e))?;
|
||||
@@ -20,11 +21,12 @@ pub fn get_sora_credentials_from_redis(
|
||||
|
||||
match (bearer, cookie, sentinel) {
|
||||
(Some(b), Some(c), Some(s)) => {
|
||||
Ok(SoraCredentials {
|
||||
bearer_token: b.to_string(),
|
||||
cookie: c.to_string(),
|
||||
sentinel: Some(s.to_string()),
|
||||
})
|
||||
let creds = SoraCredentialBuilder::new()
|
||||
.with_jwt_bearer_token(&b)
|
||||
.with_cookies(&c)
|
||||
.with_sora_sentinel(&s)
|
||||
.build()?;
|
||||
Ok(creds)
|
||||
}
|
||||
_ => Err(anyhow!("redis sora credential values not present")),
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ use mysql_queries::queries::batch_generations::insert_batch_generation_records::
|
||||
use mysql_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
|
||||
use openai_sora_client::credentials::SoraCredentials;
|
||||
use openai_sora_client::recipes::wait_for_image_gen_status::wait_for_image_gen_status;
|
||||
use openai_sora_client::requests::image_gen::image_gen_status::{Generation, TaskStatus};
|
||||
use openai_sora_client::requests::image_gen::sora_job_status::{Generation, TaskStatus};
|
||||
use r2d2_redis::r2d2::PooledConnection;
|
||||
use r2d2_redis::redis::Commands;
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
|
||||
+9
-3
@@ -124,8 +124,14 @@ pub async fn set_sora_secret_handler(
|
||||
// TODO(bt,2025-04-03): Probably not smart to return, but useful for debugging.
|
||||
Ok(Json(SetSoraSecretSuccessResponse {
|
||||
success: true,
|
||||
bearer: credentials.bearer_token,
|
||||
cookie: credentials.cookie,
|
||||
sentinel: credentials.sentinel.unwrap_or_else(|| "".to_string()),
|
||||
cookie: credentials.cookies.as_str().to_string(),
|
||||
bearer: credentials.jwt_bearer_token
|
||||
.as_ref()
|
||||
.map(|b| b.as_str().to_string())
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
sentinel: credentials.sora_sentinel
|
||||
.as_ref()
|
||||
.map(|s| s.as_str().to_string())
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
+20
-19
@@ -3,7 +3,8 @@ use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use http_server_common::request::get_request_header_optional::get_request_header_optional;
|
||||
use log::{error, info};
|
||||
use openai_sora_client::credentials::SoraCredentials;
|
||||
use openai_sora_client::creds::sora_credential_builder::SoraCredentialBuilder;
|
||||
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
|
||||
use r2d2_redis::r2d2::PooledConnection;
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
use shared_service_components::sora_redis_credentials::get_sora_credentials_from_redis::get_sora_credentials_from_redis;
|
||||
@@ -15,7 +16,7 @@ const SORA_SENTINEL_HEADER_NAME : &str = "sora-sentinel";
|
||||
pub fn get_sora_credentials_from_request(
|
||||
http_request: &HttpRequest,
|
||||
redis: &mut PooledConnection<RedisConnectionManager>,
|
||||
) -> AnyhowResult<SoraCredentials> {
|
||||
) -> AnyhowResult<SoraCredentialSet> {
|
||||
|
||||
let sora_bearer = get_request_header_optional(&http_request, SORA_BEARER_HEADER_NAME);
|
||||
let sora_cookie = get_request_header_optional(&http_request, SORA_COOKIE_HEADER_NAME);
|
||||
@@ -25,11 +26,13 @@ pub fn get_sora_credentials_from_request(
|
||||
(Some(bearer), Some(cookie), Some(sentinel)) => {
|
||||
info!("Sora credentials were present in HTTP request headers!");
|
||||
|
||||
return Ok(SoraCredentials {
|
||||
bearer_token: bearer.to_string(),
|
||||
cookie: cookie.to_string(),
|
||||
sentinel: Some(sentinel.to_string()),
|
||||
});
|
||||
let mut creds = SoraCredentialBuilder::new()
|
||||
.with_cookies(cookie)
|
||||
.with_jwt_bearer_token(bearer)
|
||||
.with_sora_sentinel(sentinel)
|
||||
.build()?;
|
||||
|
||||
return Ok(creds);
|
||||
}
|
||||
_ => {} // Fall through
|
||||
}
|
||||
@@ -37,20 +40,21 @@ pub fn get_sora_credentials_from_request(
|
||||
let redis_result = get_sora_credentials_from_redis(redis);
|
||||
|
||||
match redis_result {
|
||||
Ok(mut credentials) => {
|
||||
Ok(credentials) => {
|
||||
let mut builder = credentials.to_builder();
|
||||
if let Some(bearer) = sora_bearer {
|
||||
info!("override bearer value");
|
||||
credentials.bearer_token = bearer;
|
||||
builder = builder.with_jwt_bearer_token(&bearer);
|
||||
}
|
||||
if let Some(cookie) = sora_cookie {
|
||||
info!("override cookie value");
|
||||
credentials.cookie = cookie;
|
||||
builder = builder.with_cookies(&cookie)
|
||||
}
|
||||
if let Some(sentinel) = sora_sentinel {
|
||||
info!("override sentinel value");
|
||||
credentials.sentinel = Some(sentinel);
|
||||
builder = builder.with_sora_sentinel(&sentinel);
|
||||
}
|
||||
return Ok(credentials)
|
||||
return Ok(builder.build()?);
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Failed to get Sora credentials from Redis; using env as backup. Error: {:?}", err);
|
||||
@@ -58,11 +62,8 @@ pub fn get_sora_credentials_from_request(
|
||||
}
|
||||
};
|
||||
|
||||
match SoraCredentials::from_env() {
|
||||
Ok(creds) => Ok(creds),
|
||||
Err(err) => {
|
||||
error!("Failed to load Sora credentials from environment: {:?}", err);
|
||||
Err(anyhow!("Failed to load Sora credentials from environment"))
|
||||
}
|
||||
}
|
||||
// TODO(bt): I think I can kill this entire module. We don't call the API from our backend.
|
||||
error!("Failed to load Sora credentials from environment");
|
||||
Err(anyhow!("Failed to load Sora credentials from environment"))
|
||||
}
|
||||
|
||||
|
||||
+17
-17
@@ -14,8 +14,7 @@ use cloud_storage::bucket_client::BucketClient;
|
||||
use log::warn;
|
||||
use log::{debug, error};
|
||||
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
|
||||
use openai_sora_client::requests::image_gen::SoraImageGenError;
|
||||
use openai_sora_client::requests::sentinel_refresh::generate::token::generate_token;
|
||||
use openai_sora_client::requests::auth_sentinel::generate_sentinel_token::generate_sentinel_token;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use shared_service_components::sora_redis_credentials::keys::RedisSoraCredentialSubkey;
|
||||
@@ -36,7 +35,8 @@ use mysql_queries::payloads::generic_inference_args::generic_inference_args::{Ge
|
||||
use mysql_queries::payloads::generic_inference_args::inner_payloads::sora_image_gen_args::SoraImageGenArgs;
|
||||
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
|
||||
use openai_sora_client::error::sora_error::SoraError;
|
||||
use openai_sora_client::error::sora_specific_api_error::SoraSpecificApiError;
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
|
||||
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
|
||||
@@ -282,7 +282,7 @@ pub async fn enqueue_studio_image_generation_handler(http_request: HttpRequest,
|
||||
debug!("Uploading file {} of {} to Sora...", (i+1), files_to_upload.len());
|
||||
|
||||
let sora_upload_response =
|
||||
sora_media_upload_from_file(file_path, CredentialMigrationRef::Legacy(&sora_credentials), None)
|
||||
sora_media_upload_from_file(file_path, &sora_credentials, None)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload scene media to Sora: {:?}", err);
|
||||
@@ -317,16 +317,16 @@ pub async fn enqueue_studio_image_generation_handler(http_request: HttpRequest,
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
sora_media_tokens: sora_media_tokens.clone(),
|
||||
credentials: CredentialMigrationRef::Legacy(&sora_credentials),
|
||||
credentials: &sora_credentials,
|
||||
request_timeout: None,
|
||||
}).await;
|
||||
|
||||
debug!("Sora image gen response: {:?}", response);
|
||||
|
||||
if let Err(SoraImageGenError::SentinelBlock(msg)) = &response {
|
||||
error!("Sora sentinel block, attempting refresh: {}", msg);
|
||||
if let Err(SoraError::ApiSpecific(SoraSpecificApiError::SentinelBlockError)) = &response {
|
||||
error!("Sora sentinel block, attempting refresh");
|
||||
|
||||
match generate_token().await {
|
||||
match generate_sentinel_token().await {
|
||||
Ok(sentinel_token) => {
|
||||
set_sora_credential_field_in_redis(
|
||||
&mut redis,
|
||||
@@ -347,7 +347,7 @@ pub async fn enqueue_studio_image_generation_handler(http_request: HttpRequest,
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
sora_media_tokens: sora_media_tokens.clone(),
|
||||
credentials: CredentialMigrationRef::Legacy(&updated_sora_credentials),
|
||||
credentials: &updated_sora_credentials,
|
||||
request_timeout: None,
|
||||
}).await;
|
||||
},
|
||||
@@ -367,7 +367,7 @@ pub async fn enqueue_studio_image_generation_handler(http_request: HttpRequest,
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
sora_media_tokens: sora_media_tokens.clone(),
|
||||
credentials: CredentialMigrationRef::Legacy(&updated_sora_credentials),
|
||||
credentials: &updated_sora_credentials,
|
||||
request_timeout: None,
|
||||
}).await;
|
||||
},
|
||||
@@ -381,19 +381,19 @@ pub async fn enqueue_studio_image_generation_handler(http_request: HttpRequest,
|
||||
|
||||
let response = response.map_err(|err| {
|
||||
match err {
|
||||
SoraImageGenError::TokenExpired(msg) => {
|
||||
error!("Sora token expired, needs refresh: {}", msg);
|
||||
SoraError::ApiSpecific(SoraSpecificApiError::TokenExpiredError) => {
|
||||
error!("Sora token expired, needs refresh");
|
||||
// TODO: Implement token refresh logic here
|
||||
EnqueueImageGenRequestError::ServerError
|
||||
},
|
||||
SoraImageGenError::SentinelBlock(msg) => {
|
||||
error!("Sora sentinel block still occurring after refresh: {}", msg);
|
||||
SoraError::ApiSpecific(SoraSpecificApiError::SentinelBlockError) => {
|
||||
error!("Sora sentinel block still occurring after refresh");
|
||||
EnqueueImageGenRequestError::ServerError
|
||||
},
|
||||
SoraImageGenError::TooManyConcurrentTasks(msg) => {
|
||||
error!("Sora too many concurrent tasks: {}", msg);
|
||||
SoraError::ApiSpecific(SoraSpecificApiError::TooManyConcurrentTasks) => {
|
||||
error!("Sora too many concurrent tasks");
|
||||
EnqueueImageGenRequestError::TooManyConcurrentTasks
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
error!("Failed to call Sora image generation: {:?}", err);
|
||||
EnqueueImageGenRequestError::ServerError
|
||||
|
||||
Reference in New Issue
Block a user