mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17071ab714 | ||
|
|
9553a91bc3 | ||
|
|
eb2969cfd8 | ||
|
|
855463f9d8 |
Generated
+29
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+14
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user