openart api

This commit is contained in:
Brandon Thomas committed 2025-05-30 09:09:47 -04:00
1 parent 6a01e4bba0
commit 855463f9d8
14 files changed
+327

No files matched your search

Generated
+30
View File
@@ -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"
+1
View File
@@ -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",
@@ -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" }
@@ -0,0 +1,2 @@
pub mod openart_cookies;
pub mod openart_credentials;
@@ -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()
}
}
@@ -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<OpenArtCookies>,
}
impl OpenArtCredentials {
pub fn from_cookies(cookies: &str) -> Self {
let cookies = OpenArtCookies::new(cookies.to_string());
Self {
cookies: Some(cookies),
}
}
}
@@ -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<JwtError> for ApiError {
fn from(err: JwtError) -> Self {
ApiError::JwtError(err)
}
}
@@ -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 })
}
@@ -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 {}
@@ -0,0 +1,4 @@
pub mod api_error;
pub mod classify_http_error_response;
pub mod client_error;
pub mod openart_error;
@@ -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<ApiError> for OpenArtError {
fn from(err: ApiError) -> Self {
OpenArtError::Api(err)
}
}
impl From<ClientError> for OpenArtError {
fn from(err: ClientError) -> Self {
OpenArtError::Client(err)
}
}
@@ -0,0 +1,3 @@
pub mod creds;
pub mod error;
pub mod requests;
@@ -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<String, OpenArtError> {
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)
}
@@ -0,0 +1 @@
pub mod get_session_request;