Compare commits

...
Author SHA1 Message Date
Brandon Thomas 17071ab714 uploads work 2025-05-30 10:25:06 -04:00
Brandon Thomas 9553a91bc3 session details 2025-05-30 09:35:14 -04:00
Brandon Thomas eb2969cfd8 session endpoint 2025-05-30 09:21:43 -04:00
Brandon Thomas 855463f9d8 openart api 2025-05-30 09:09:47 -04:00
17 changed files with 612 additions and 0 deletions
Generated
+29
View File
@@ -6834,6 +6834,35 @@ 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",
"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,45 @@
[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" }
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,3 @@
pub mod openart_cookies;
pub mod openart_credentials;
pub mod openart_session_info;
@@ -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,18 @@
use crate::creds::openart_cookies::OpenArtCookies;
use crate::creds::openart_session_info::OpenArtSessionInfo;
#[derive(Clone)]
pub struct OpenArtCredentials {
pub cookies: Option<OpenArtCookies>,
pub session_info: Option<OpenArtSessionInfo>,
}
impl OpenArtCredentials {
pub fn from_cookies(cookies: &str) -> Self {
let cookies = OpenArtCookies::new(cookies.to_string());
Self {
cookies: Some(cookies),
session_info: None,
}
}
}
@@ -0,0 +1,10 @@
#[derive(Debug, Clone)]
pub struct OpenArtSessionInfo {
/// This is either a session ID, user ID, or subscription ID.
/// It is passed as the header `X-USER-ID` in other requests.
pub sub: Option<String>,
pub email: Option<String>,
pub name: Option<String>,
pub image: Option<String>,
}
@@ -0,0 +1,37 @@
use reqwest::StatusCode;
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum ApiError {
/// Session endpoint did not return a valid session id.
/// Specifically for calls to https://openart.ai/api/auth/session
InvalidSession,
CouldNotParseSession { error: serde_json::Error, body: String },
/// 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::InvalidSession => write!(f, "Invalid session: no session id returned from the server"),
ApiError::CouldNotParseSession { error, body } => {
write!(f, "Could not parse session: {}. Body: {}", error, body)
}
ApiError::ReqwestError(err) => write!(f, "ReqwestError: {}", err),
ApiError::UncategorizedBadResponse { status_code, message } => {
write!(f, "UncategorizedBadResponse: {:?} : {}", status_code, message)
},
}
}
}
impl Error for ApiError {}
@@ -0,0 +1,15 @@
use crate::error::api_error::ApiError;
use crate::error::classify_http_error_status_code_and_body::classify_http_error_status_code_and_body;
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)),
};
classify_http_error_status_code_and_body(status, &message).await
}
@@ -0,0 +1,14 @@
use crate::error::api_error::ApiError;
use crate::error::openart_error::OpenArtError;
use reqwest::StatusCode;
/// This assumes the request failed and returned a non-200.
/// The caller should check.
pub async fn classify_http_error_status_code_and_body(status: StatusCode, body: &str) -> OpenArtError {
let message = body.to_string();
// TODO: Handle various error messages from the API.
OpenArtError::Api(ApiError::UncategorizedBadResponse { status_code: status, message })
}
@@ -0,0 +1,34 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum ClientError {
/// No cookies in credentials
NoCookiesInCredentials,
/// No session in the credentials
NoSessionInfoInCredentials,
/// An error that occurred clientside before the request was sent.
ReqwestError(reqwest::Error),
/// An I/O error, e.g. reading from a file or network stream.
IoError(std::io::Error),
/// Another error that does not fit the above categories.
Other(String),
}
impl Display for ClientError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoCookiesInCredentials => write!(f, "No cookies are present in the credentials"),
Self::NoSessionInfoInCredentials => write!(f, "No session in the credentials"),
Self::ReqwestError(err) => write!(f, "Reqwest error: {}", err),
Self::IoError(err) => write!(f, "I/O error: {}", err),
Self::Other(message) => write!(f, "Other client error: {}", message),
}
}
}
impl Error for ClientError {}
@@ -0,0 +1,5 @@
pub mod api_error;
pub mod classify_http_error_response;
pub mod client_error;
pub mod openart_error;
pub mod classify_http_error_status_code_and_body;
@@ -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,165 @@
/*
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 crate::creds::openart_credentials::OpenArtCredentials;
use crate::error::api_error::ApiError;
use crate::error::classify_http_error_status_code_and_body::classify_http_error_status_code_and_body;
use crate::error::client_error::ClientError;
use crate::error::openart_error::OpenArtError;
use chrono::{DateTime, Utc};
use log::{debug, error, info};
use reqwest::Client;
use serde_derive::Deserialize;
const SESSION_URL : &str = "https://openart.ai/api/auth/session";
#[derive(Debug, Clone)]
pub struct SessionDetails {
/// This is either a session ID, user ID, or subscription ID.
/// It is passed as the header `X-USER-ID` in other requests.
pub sub: Option<String>,
pub email: Option<String>,
pub name: Option<String>,
pub image: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
struct RawSession {
user: Option<RawUser>,
expires: Option<DateTime<Utc>>,
/// This is either a session ID, user ID, or subscription ID.
/// It is passed as the header `X-USER-ID` in other requests.
sub: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
struct RawUser {
name: Option<String>,
email: Option<String>,
image: Option<String>,
}
pub async fn get_session_request(creds: &OpenArtCredentials) -> Result<SessionDetails, 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 details 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 read session details: {:?}", err);
ApiError::ReqwestError(err)
})?;
if !status.is_success() {
error!("Failed to get session details: {} ; body = {}", status, response_body);
let error = classify_http_error_status_code_and_body(status, &response_body).await;
return Err(error);
}
debug!("Session info response was 200. Body: {}", response_body);
if response_body.is_empty() || response_body == "{}" {
error!("Received empty response body when requesting session details.");
return Err(OpenArtError::Api(ApiError::InvalidSession));
}
let session : RawSession = serde_json::from_str(response_body)
.map_err(|err| {
error!("Failed to parse session details: {} body: {}", err, response_body);
OpenArtError::Api(ApiError::CouldNotParseSession { error: err, body: response_body.to_string() } )
})?;
Ok(SessionDetails {
sub: session.sub,
email: session.user.as_ref().and_then(|u| u.email.clone()),
name: session.user.as_ref().and_then(|u| u.name.clone()),
image: session.user.as_ref().and_then(|u| u.image.clone()),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::openart_cookies::OpenArtCookies;
use crate::creds::openart_credentials::OpenArtCredentials;
#[tokio::test]
#[ignore] // Do not run in CI. Run manully to test session retrieval.
async fn invalid_session() {
let creds = OpenArtCredentials {
cookies: Some(OpenArtCookies::new("".to_string())),
session_info: None,
};
let result = get_session_request(&creds).await;
println!("Result: {:?}", result);
assert!(result.is_err());
}
#[tokio::test]
#[ignore] // Do not run in CI. Run manully to test session retrieval.
async fn valid_session() {
let 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";
let creds = OpenArtCredentials {
cookies: Some(OpenArtCookies::new(cookie.to_string())),
session_info: None,
};
let result = get_session_request(&creds).await;
println!("Result: {:?}", result);
assert!(result.is_ok());
}
}
@@ -0,0 +1,2 @@
pub mod get_session_request;
pub mod upload_image_from_file_request;
@@ -0,0 +1,174 @@
/*
curl 'https://openart.ai/api/media/upload_raw_image' --compressed
-X POST
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0'
-H 'Accept: application/json, text/plain, * / *'
-H 'Accept-Language: en-US,en;q=0.5'
-H 'Accept-Encoding: gzip, deflate, br, zstd'
-H 'X-USER-ID: 6abuz85cRo1iVAmk6Rjf'
-H 'Content-Type: multipart/form-data; boundary=----geckoformboundary1bbe4d86f5d33a00827b7d293784b7ec'
-H 'Origin: https://openart.ai'
-H 'Connection: keep-alive'
-H 'Referer: https://openart.ai/create?mode=edit&imageAction=changeBackground&action_mode=background_manual_remove'
-H 'Cookie: AMP_3e2fda7a5c=JTdCJTIyZGV2aWNlSWQlMjIlM0ElMjJmZmRkZTQ2MS1jMmVlLTRmNmUtYmQwMi0zNDcwYjY1OWNkMTklMjIlMkMlMjJ1c2VySWQlMjIlM0ElMjI2YWJ1ejg1Y1JvMWlWQW1rNlJqZiUyMiUyQyUyMnNlc3Npb25JZCUyMiUzQTE3NDg2MTI1MjI4NjglMkMlMjJvcHRPdXQlMjIlM0FmYWxzZSUyQyUyMmxhc3RFdmVudFRpbWUlMjIlM0ExNzQ4NjEyNTIzMDI4JTJDJTIybGFzdEV2ZW50SWQlMjIlM0EzNzAlMkMlMjJwYWdlQ291bnRlciUyMiUzQTElN0Q=; 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..3_1LkfPK4sWQd3GW.KZ7yqoe7fdPEDoTrekIxFIxs5hZVn9Rg8WfsS4tD4Ana6MQmn_LnFhvF-cKAd187Yh3ULWozsFpZAOIQbnndPPsZ5sYu5pYXbQypN5NGC_movTNVQOvzp6nv5r4l0Of1vMslik_wa-LPZZc-blTNGeW-E9Zy35IpK2gHruTAXV_HPfw9uA9OA_RlI3BDxveqnooBdJ1PiU7fJ_MQe6A8Q1XyJ1OtyatXlIBnvrCVuHDPS0ChZbnUA7OTD_d0O8OVyy5yAo-qGer9j_kLg8aC0e-96DUOHq7hwjQyfMDssPVfv1vuArwsZXUThfsb60DGYjDnTIsZlzru-eTghxjrJJuUo7PYRi1A71E4Nu69Ds0.dKUcrC_sRmBDIzp_lew8EQ'
-H 'Sec-Fetch-Dest: empty'
-H 'Sec-Fetch-Mode: cors'
-H 'Sec-Fetch-Site: same-origin'
-H 'Pragma: no-cache'
-H 'Cache-Control: no-cache'
-H 'TE: trailers' --data-binary $'------geckoformboundary1bbe4d86f5d33a00827b7d293784b7ec\r\n
//
// Content-Disposition: form-data; name="file"; filename="blob"\r\nContent-Type: image/png\r\n\r\n------geckoformboundary1bbe4d86f5d33a00827b7d293784b7ec--\r\n'
{"imageUrl":"https://cdn.openart.ai/production/2025-05/uploads/anonymous/image_A-0v2jOD_1536x1024_1748612534709.png","width":1536,"height":1024}
*/
#[derive(Clone, Debug, Deserialize)]
struct RawUploadResponse {
#[serde(rename = "imageUrl")]
image_url: Option<String>,
width: Option<i64>,
height: Option<i64>,
}
use crate::creds::openart_credentials::OpenArtCredentials;
use crate::error::client_error::ClientError;
use crate::error::openart_error::OpenArtError;
use log::info;
use reqwest::multipart::{Form, Part};
use reqwest::Client;
use std::path::Path;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use crate::error::api_error::ApiError;
use crate::error::classify_http_error_response::classify_http_error_response;
/// Try to prevent buffer reallocations.
/// There's a better way to implement this.
const INITIAL_BUFFER_SIZE : usize = 1024*1024;
const UPLOAD_URL: &str = "https://openart.ai/api/media/upload_raw_image";
pub async fn upload_image_from_file_request<P: AsRef<Path>>(
file_path: P,
creds: &OpenArtCredentials,
maybe_timeout: Option<Duration>,
) -> Result<(), OpenArtError> {
let filename = file_path.as_ref().file_name()
.ok_or_else(|| OpenArtError::Client(ClientError::Other("Could not determine filename from path".to_string())))?
.to_string_lossy()
.to_string();
let mut file = File::open(&file_path)
.await
.map_err(|err| OpenArtError::Client(ClientError::IoError(err)))?;
let mut file_bytes = Vec::with_capacity(INITIAL_BUFFER_SIZE);
file.read_to_end(&mut file_bytes)
.await
.map_err(|err| OpenArtError::Client(ClientError::IoError(err)))?;
// 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()) {
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("png") => "image/png",
// Some("webp") => "image/webp",
// Some("gif") => "image/gif",
// Some("mp4") => "video/mp4",
// Some("mov") => "video/quicktime",
// Some("webm") => "video/webm",
_ => "application/octet-stream",
};
// 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)
.map_err(|err| OpenArtError::Client(ClientError::ReqwestError(err)))?;
let form = Form::new().part("file", part);
let cookie = creds.cookies.as_ref()
.map(|cookies| cookies.to_string())
.ok_or_else(|| OpenArtError::Client(ClientError::NoCookiesInCredentials))?;
let session_id = creds.session_info.as_ref()
.map(|info| info.sub.clone())
.flatten()
.ok_or_else(|| OpenArtError::Client(ClientError::NoSessionInfoInCredentials))?;
// Make API request
let client = Client::new();
let mut request_builder = client.post(UPLOAD_URL)
.multipart(form)
.header("X-USER-ID", session_id)
.header("Cookie", &cookie);
if let Some(timeout) = maybe_timeout {
request_builder = request_builder.timeout(timeout);
}
let response = request_builder
.send()
.await
.map_err(|err| OpenArtError::Api(ApiError::ReqwestError(err)))?;
// Check response status
if !response.status().is_success() {
info!("Error uploading image: {:?}", response.status());
let error = classify_http_error_response(response).await;
return Err(error);
}
// Parse response
let upload_response = response.json::<RawUploadResponse>()
.await
.map_err(|err| OpenArtError::Api(ApiError::ReqwestError(err)))?;
println!("Uploaded data: {:?}", upload_response);
Ok(())
}
#[cfg(test)]
mod tests {
use errors::AnyhowResult;
use testing::test_file_path::test_file_path;
use crate::creds::openart_cookies::OpenArtCookies;
use crate::creds::openart_credentials::OpenArtCredentials;
use crate::creds::openart_session_info::OpenArtSessionInfo;
use crate::requests::get_session_request::{get_session_request, SessionDetails};
use crate::requests::upload_image_from_file_request::upload_image_from_file_request;
#[tokio::test]
#[ignore] // Do not run in CI. Run manully to test session retrieval.
async fn test() -> AnyhowResult<()> {
let 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";
let mut creds = OpenArtCredentials {
cookies: Some(OpenArtCookies::new(cookie.to_string())),
session_info: None,
};
let session_details = get_session_request(&creds).await.unwrap();
creds.session_info = Some(OpenArtSessionInfo {
sub: session_details.sub.clone(),
email: None,
name: None,
image: None,
});
let image_path = test_file_path("test_data/image/juno.jpg")?; // media_01jqyqgqpwf40tkcapq5bmaz5d
let result = upload_image_from_file_request(image_path, &creds, None).await;
assert!(result.is_ok());
Ok(())
}
}