diff --git a/Cargo.lock b/Cargo.lock index 6db3910d47..fdf8e48659 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6834,6 +6834,36 @@ dependencies = [ "workspace-build-acceleration", ] +[[package]] +name = "openart_client" +version = "0.0.1" +dependencies = [ + "anyhow", + "base64 0.22.1", + "chrono", + "cookie 0.18.1", + "crockford", + "errors", + "filesys", + "hex-literal", + "idempotency", + "jwt_light", + "log", + "once_cell", + "rand 0.9.1", + "reqwest 0.12.15", + "serde", + "serde_derive", + "serde_json", + "sha3", + "subprocess_common", + "testing", + "thiserror 2.0.12", + "tokio", + "toml 0.8.22", + "workspace-build-acceleration", +] + [[package]] name = "openssl" version = "0.10.71" diff --git a/Cargo.toml b/Cargo.toml index dfe1da0ae4..0beeb796cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "crates/lib/api_clients/google_drive_common", "crates/lib/api_clients/google_sign_in", "crates/lib/api_clients/openai_sora_client", + "crates/lib/api_clients/openart_client", "crates/lib/api_clients/runwayml_client", "crates/lib/api_clients/storyteller_client", "crates/lib/api_clients/thumbnail_generator", diff --git a/crates/lib/api_clients/openart_client/Cargo.toml b/crates/lib/api_clients/openart_client/Cargo.toml new file mode 100644 index 0000000000..0fdac776d1 --- /dev/null +++ b/crates/lib/api_clients/openart_client/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "openart_client" +edition = "2024" +version = "0.0.1" +publish = false + +[package.metadata.cargo-udeps.ignore] +normal = ["workspace-build-acceleration"] + +[lib] +name = "openart_client" +path = "src/lib.rs" + +[dependencies] + +# Workspace hack (faster compile times) +workspace-build-acceleration = { workspace = true } + +# Internal +crockford = { path = "../../data/crockford" } +errors = { path = "../../errors" } +idempotency = { path = "../../idempotency" } +jwt_light = { workspace = true } +subprocess_common = { path = "../../subprocess_common" } + +# External +anyhow = { workspace = true } +base64 = { workspace = true } +chrono = { workspace = true} +cookie = { workspace = true } +hex-literal = { workspace = true } +log = { workspace = true } +once_cell= { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true, features = ["multipart", "gzip"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } +sha3 = { workspace = true } +thiserror = { workspace = true } +tokio = { version = "1.38.0", features = ["macros"] } +toml = { workspace = true } + +[dev-dependencies] +testing = { path = "../../testing" } +filesys = { path = "../../files/filesys" } diff --git a/crates/lib/api_clients/openart_client/src/creds/mod.rs b/crates/lib/api_clients/openart_client/src/creds/mod.rs new file mode 100644 index 0000000000..aeac9c6f2c --- /dev/null +++ b/crates/lib/api_clients/openart_client/src/creds/mod.rs @@ -0,0 +1,2 @@ +pub mod openart_cookies; +pub mod openart_credentials; \ No newline at end of file diff --git a/crates/lib/api_clients/openart_client/src/creds/openart_cookies.rs b/crates/lib/api_clients/openart_client/src/creds/openart_cookies.rs new file mode 100644 index 0000000000..d6b0013a5a --- /dev/null +++ b/crates/lib/api_clients/openart_client/src/creds/openart_cookies.rs @@ -0,0 +1,24 @@ + +/// The cookies payload is the entire cookie header (potentially many cookies). +#[derive(Clone)] +pub struct OpenArtCookies { + cookies: String, +} + +impl OpenArtCookies { + pub fn new(cookies: String) -> Self { + OpenArtCookies { cookies } + } + + pub fn as_str(&self) -> &str { + &self.cookies + } + + pub fn as_bytes(&self) -> &[u8] { + self.cookies.as_bytes() + } + + pub fn to_string(&self) -> String { + self.cookies.clone() + } +} diff --git a/crates/lib/api_clients/openart_client/src/creds/openart_credentials.rs b/crates/lib/api_clients/openart_client/src/creds/openart_credentials.rs new file mode 100644 index 0000000000..19ed39887e --- /dev/null +++ b/crates/lib/api_clients/openart_client/src/creds/openart_credentials.rs @@ -0,0 +1,17 @@ +use crate::creds::jwt_bearer_token::JwtBearerToken; +use crate::creds::openart_cookies::OpenArtCookies; +use crate::error::api_error::ApiError; + +#[derive(Clone)] +pub struct OpenArtCredentials { + pub cookies: Option, +} + +impl OpenArtCredentials { + pub fn from_cookies(cookies: &str) -> Self { + let cookies = OpenArtCookies::new(cookies.to_string()); + Self { + cookies: Some(cookies), + } + } +} diff --git a/crates/lib/api_clients/openart_client/src/error/api_error.rs b/crates/lib/api_clients/openart_client/src/error/api_error.rs new file mode 100644 index 0000000000..b56b21d869 --- /dev/null +++ b/crates/lib/api_clients/openart_client/src/error/api_error.rs @@ -0,0 +1,36 @@ +use jwt_light::error::JwtError; +use reqwest::StatusCode; +use std::error::Error; +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub enum ApiError { + /// An error that occurred as or after the request was sent. + ReqwestError(reqwest::Error), + + /// Some other failure response. + UncategorizedBadResponse{ + status_code: StatusCode, + message : String, + } +} + +impl Display for ApiError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ApiError::JwtError(err) => write!(f, "JwtError: {}", err), + ApiError::ReqwestError(err) => write!(f, "ReqwestError: {}", err), + ApiError::UncategorizedBadResponse { status_code, message } => { + write!(f, "UncategorizedBadResponse: {:?} : {}", status_code, message) + } + } + } +} + +impl Error for ApiError {} + +impl From for ApiError { + fn from(err: JwtError) -> Self { + ApiError::JwtError(err) + } +} diff --git a/crates/lib/api_clients/openart_client/src/error/classify_http_error_response.rs b/crates/lib/api_clients/openart_client/src/error/classify_http_error_response.rs new file mode 100644 index 0000000000..d85c78006f --- /dev/null +++ b/crates/lib/api_clients/openart_client/src/error/classify_http_error_response.rs @@ -0,0 +1,16 @@ +use crate::error::api_error::ApiError; +use crate::error::openart_error::OpenArtError; + +/// This assumes the request failed and returned a non-200. +/// The caller should check. +pub async fn classify_http_error_response(response: reqwest::Response) -> OpenArtError { + let status = response.status(); + let message = match response.text().await { + Ok(text) => text, + Err(err) => return OpenArtError::Api(ApiError::ReqwestError(err)), + }; + + // TODO: Handle various error messages from the API. + + OpenArtError::Api(ApiError::UncategorizedBadResponse { status_code: status, message }) +} diff --git a/crates/lib/api_clients/openart_client/src/error/client_error.rs b/crates/lib/api_clients/openart_client/src/error/client_error.rs new file mode 100644 index 0000000000..2e31f912e3 --- /dev/null +++ b/crates/lib/api_clients/openart_client/src/error/client_error.rs @@ -0,0 +1,26 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub enum ClientError { + /// No cookies in credentials + NoCookiesInCredentials, + + /// No JWT bearer token in the credentials + NoJwtBearerToken, + + /// An error that occurred clientside before the request was sent. + ReqwestError(reqwest::Error), +} + +impl Display for ClientError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ClientError::NoCookiesInCredentials => write!(f, "No cookies are present in the credentials"), + ClientError::NoJwtBearerToken => write!(f, "No JWT bearer token in the credentials"), + ClientError::ReqwestError(err) => write!(f, "Reqwest error: {}", err), + } + } +} + +impl Error for ClientError {} diff --git a/crates/lib/api_clients/openart_client/src/error/mod.rs b/crates/lib/api_clients/openart_client/src/error/mod.rs new file mode 100644 index 0000000000..7a7e487915 --- /dev/null +++ b/crates/lib/api_clients/openart_client/src/error/mod.rs @@ -0,0 +1,4 @@ +pub mod api_error; +pub mod classify_http_error_response; +pub mod client_error; +pub mod openart_error; \ No newline at end of file diff --git a/crates/lib/api_clients/openart_client/src/error/openart_error.rs b/crates/lib/api_clients/openart_client/src/error/openart_error.rs new file mode 100644 index 0000000000..e3891f40e4 --- /dev/null +++ b/crates/lib/api_clients/openart_client/src/error/openart_error.rs @@ -0,0 +1,33 @@ +use crate::error::api_error::ApiError; +use crate::error::client_error::ClientError; +use std::error::Error; +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub enum OpenArtError { + Api(ApiError), + Client(ClientError), +} + +impl Display for OpenArtError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Api(err) => write!(f, "ApiError: {}", err), + Self::Client(err) => write!(f, "ClientError: {}", err), + } + } +} + +impl Error for OpenArtError {} + +impl From for OpenArtError { + fn from(err: ApiError) -> Self { + OpenArtError::Api(err) + } +} + +impl From for OpenArtError { + fn from(err: ClientError) -> Self { + OpenArtError::Client(err) + } +} diff --git a/crates/lib/api_clients/openart_client/src/lib.rs b/crates/lib/api_clients/openart_client/src/lib.rs new file mode 100644 index 0000000000..c3a1a5d340 --- /dev/null +++ b/crates/lib/api_clients/openart_client/src/lib.rs @@ -0,0 +1,3 @@ +pub mod creds; +pub mod error; +pub mod requests; \ No newline at end of file diff --git a/crates/lib/api_clients/openart_client/src/requests/get_session_request.rs b/crates/lib/api_clients/openart_client/src/requests/get_session_request.rs new file mode 100644 index 0000000000..d4823b9e28 --- /dev/null +++ b/crates/lib/api_clients/openart_client/src/requests/get_session_request.rs @@ -0,0 +1,88 @@ +/* +curl 'https://openart.ai/api/auth/session' --compressed + -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0' + -H 'Accept: * / *' + -H 'Accept-Language: en-US,en;q=0.5' + -H 'Accept-Encoding: gzip, deflate, br, zstd' + -H 'Referer: https://openart.ai/create?mode=edit&imageAction=changeBackground&action_mode=background_manual_remove' + -H 'Content-Type: application/json' + -H 'Connection: keep-alive' + -H 'Cookie: AMP_3e2fda7a5c=JTdCJTIyZGV2aWNlSWQlMjIlM0ElMjJmZmRkZTQ2MS1jMmVlLTRmNmUtYmQwMi0zNDcwYjY1OWNkMTklMjIlMkMlMjJ1c2VySWQlMjIlM0ElMjI2YWJ1ejg1Y1JvMWlWQW1rNlJqZiUyMiUyQyUyMnNlc3Npb25JZCUyMiUzQTE3NDg1ODQyODY0NzclMkMlMjJvcHRPdXQlMjIlM0FmYWxzZSUyQyUyMmxhc3RFdmVudFRpbWUlMjIlM0ExNzQ4NTg0NTU5NzEyJTJDJTIybGFzdEV2ZW50SWQlMjIlM0EzNjglMkMlMjJwYWdlQ291bnRlciUyMiUzQTklN0Q=; AMP_MKTG_3e2fda7a5c=JTdCJTIycmVmZXJyZXIlMjIlM0ElMjJodHRwcyUzQSUyRiUyRnd3dy5nb29nbGUuY29tJTJGJTIyJTJDJTIycmVmZXJyaW5nX2RvbWFpbiUyMiUzQSUyMnd3dy5nb29nbGUuY29tJTIyJTdE; __client_uat=0; __client_uat_Xt7g_-Hi=0; utm_params={%22utm_source%22:%22organic%22%2C%22utm_medium%22:%22Google%22%2C%22utm_campaign%22:%22oa_unknown%22%2C%22utm_term%22:%22oa_unknown%22%2C%22utm_content%22:%22oa_unknown%22}; themeMode=dark; themeDirection=ltr; themeColorPresets=default; themeLayout=horizontal; themeContrast=default; themeStretch=false; unique_device_id=f1f0b393-815f-4f95-aaa2-65a3b94530f6; __Host-next-auth.csrf-token=ea06d49af6d70cbe0d15774565708d6c394790c5ab2610276bb0bf29a4afb47b%7Cbdbf15d7f2bf44c83586c7ab1ec47ea6c114b26ccc8e20ae45f40f06c0377389; __Secure-next-auth.callback-url=https%3A%2F%2Fopenart.ai%2Fcreate; __Secure-next-auth.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..mzdySrH34fIj41NP.ky2ZVPIugA1EWSKL29EEEKvSxfG4LCo-R7rN-yLzLo-2LmCrFzVe15BCH2MYg90cwkdIdm1Hi-7U4BcnxG0x662UrU9RDw2yX_ZTZge6Kz70-pg1TaVvKWOS_Gibv8ERSK6MHTfqlx4WNvHccOOfDIWhN87zbLXHCbWnexgmBOB3XfMA96Hby55JNgDM3-_JPcg1lFNkT8oAW562FUZxM9EMzKD4-A4Ee1ZpVd0Z51k4lqfS4XED-0xT6xagsCd6CdwEHEHc0paIAj34Kb_lCO2nyrxGBYvx1XlVHjvXdtakrTqe6jrOuV5rQ0iEO6Xk6cibcZydMV4GssKPEwUDS718AJCxNUK9yHi4RRyfYus.bhvOrrfLdszMcHfJ3Ex2wA' + -H 'Sec-Fetch-Dest: empty' + -H 'Sec-Fetch-Mode: cors' + -H 'Sec-Fetch-Site: same-origin' + -H 'Priority: u=4' + -H 'Pragma: no-cache' + -H 'Cache-Control: no-cache' + -H 'TE: trailers' + */ +use log::{error, info}; +use crate::creds::openart_credentials::OpenArtCredentials; +use crate::error::openart_error::OpenArtError; +use reqwest::Client; +use crate::error::api_error::ApiError; +use crate::error::client_error::ClientError; + +const SESSION_URL : &str = "https://openart.ai/api/auth/session"; + +pub async fn get_session_request(creds: &OpenArtCredentials) -> Result { + + let cookies = match creds.cookies.as_ref() { + Some(cookies) => cookies, + None => { + error!("Failed to request session. No cookies in credentials."); + return Err(ClientError::NoCookiesInCredentials.into()); + } + }; + + let client = Client::builder() + .gzip(true) + .build() + .map_err(|err| { + error!("Failed to create HTTP client: {}", err); + OpenArtError::Client(ClientError::ReqwestError(err)) + })?; + + info!("Getting session info from cookies... (cookie payload length: {})", cookies.as_str().len()); + + let mut http_request= client.get(SESSION_URL) + .header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0") + .header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate, br") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cookie", cookies.as_str()); + + let http_request = http_request.build() + .map_err(|err| ApiError::ReqwestError(err))?; + + let response = client.execute(http_request) + .await + .map_err(|err| ApiError::ReqwestError(err))?; + + let status = response.status(); + + let response_body = &response.text() + .await + .map_err(|err| { + error!("Error reading body while attempting to generate bearer token: {:?}", err); + ApiError::ReqwestError(err) + })?; + + if !status.is_success() { + error!("Failed to generate bearer token with session cookies: {} ; body = {}", status, response_body); + let error = classify_general_http_status_code_and_body(status, &response_body).await; + return Err(error); + } + + debug!("Bearer token generation response was 200."); + debug!("Auth Response: {}", response_body); + + if response_body == "null" { + error!("Failed to generate bearer token. Response was the string `null`."); + return Err(SoraError::FailedToGenerateBearer) + } + + let auth_response: SoraAuthResponse = serde_json::from_str(&response_body)?; + + Ok(auth_response.access_token) +} diff --git a/crates/lib/api_clients/openart_client/src/requests/mod.rs b/crates/lib/api_clients/openart_client/src/requests/mod.rs new file mode 100644 index 0000000000..6b3ef7d51a --- /dev/null +++ b/crates/lib/api_clients/openart_client/src/requests/mod.rs @@ -0,0 +1 @@ +pub mod get_session_request; \ No newline at end of file