use crate::api::error::ApiError;
use mime_guess::from_path;
use reqwest::Client;
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
use std::path::PathBuf;
use std::time::Duration;
const DEFAULT_API_BASE: &str = "https://www.moltbook.com/api/v1";
pub struct MoltbookClient {
client: Client,
api_key: String,
pub agent_name: String,
debug: bool,
base_url: String,
}
impl MoltbookClient {
pub fn new(api_key: String, agent_name: String, debug: bool) -> Self {
Self {
client: Client::builder()
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.build()
.expect("Failed to build HTTP client"),
api_key,
agent_name,
debug,
base_url: DEFAULT_API_BASE.to_string(),
}
}
pub fn with_base_url(mut self, base_url: String) -> Self {
self.base_url = base_url;
self
}
pub async fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, ApiError> {
let url = format!("{}{}", self.base_url, endpoint);
if self.debug {
eprintln!("GET {}", url);
}
let response = self
.client
.get(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.send()
.await?;
self.handle_response(response).await
}
pub async fn post<T: DeserializeOwned>(
&self,
endpoint: &str,
body: &impl Serialize,
) -> Result<T, ApiError> {
let url = format!("{}{}", self.base_url, endpoint);
if self.debug {
eprintln!("POST {}", url);
eprintln!(
"Body: {}",
serde_json::to_string_pretty(&body).unwrap_or_default()
);
}
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(body)
.send()
.await?;
self.handle_response(response).await
}
pub async fn post_unauth<T: DeserializeOwned>(
&self,
endpoint: &str,
body: &impl Serialize,
) -> Result<T, ApiError> {
let url = format!("{}{}", self.base_url, endpoint);
if self.debug {
eprintln!("POST (unauth) {}", url);
eprintln!(
"Body: {}",
serde_json::to_string_pretty(&body).unwrap_or_default()
);
}
let response = self
.client
.post(&url)
.header("Content-Type", "application/json")
.json(body)
.send()
.await?;
self.handle_response(response).await
}
pub async fn post_file<T: DeserializeOwned>(
&self,
endpoint: &str,
file_path: PathBuf,
) -> Result<T, ApiError> {
let url = format!("{}{}", self.base_url, endpoint);
let file_name = file_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let file_contents = std::fs::read(&file_path).map_err(ApiError::IoError)?;
let mime_type = from_path(&file_path).first_or_octet_stream();
let part = reqwest::multipart::Part::bytes(file_contents)
.file_name(file_name)
.mime_str(mime_type.as_ref())?;
let form = reqwest::multipart::Form::new().part("file", part);
if self.debug {
eprintln!("POST (File) {}", url);
eprintln!("File: {:?}", file_path);
}
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.multipart(form)
.send()
.await?;
self.handle_response(response).await
}
pub async fn patch<T: DeserializeOwned>(
&self,
endpoint: &str,
body: &impl Serialize,
) -> Result<T, ApiError> {
let url = format!("{}{}", self.base_url, endpoint);
if self.debug {
eprintln!("PATCH {}", url);
eprintln!(
"Body: {}",
serde_json::to_string_pretty(&body).unwrap_or_default()
);
}
let response = self
.client
.patch(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(body)
.send()
.await?;
self.handle_response(response).await
}
pub async fn delete<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, ApiError> {
let url = format!("{}{}", self.base_url, endpoint);
if self.debug {
eprintln!("DELETE {}", url);
}
let response = self
.client
.delete(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.send()
.await?;
self.handle_response(response).await
}
async fn handle_response<T: DeserializeOwned>(
&self,
response: reqwest::Response,
) -> Result<T, ApiError> {
let status = response.status();
let text = response.text().await?;
if self.debug {
eprintln!("Response Status: {}", status);
eprintln!("Response Body: {}", text);
}
if status.as_u16() == 429 {
if let Ok(json) = serde_json::from_str::<Value>(&text) {
if let Some(retry) = json.get("retry_after_minutes").and_then(|v| v.as_u64()) {
return Err(ApiError::RateLimited(format!("{} minutes", retry)));
}
if let Some(retry) = json.get("retry_after_seconds").and_then(|v| v.as_u64()) {
return Err(ApiError::RateLimited(format!("{} seconds", retry)));
}
}
return Err(ApiError::RateLimited("Wait before retrying".to_string()));
}
if !status.is_success() {
if let Ok(json) = serde_json::from_str::<Value>(&text) {
let error = json
.get("error")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
if error == "captcha_required" {
let token = json
.get("token")
.and_then(|v| v.as_str())
.unwrap_or("unknown_token");
return Err(ApiError::CaptchaRequired(token.to_string()));
}
let hint = json.get("hint").and_then(|v| v.as_str()).unwrap_or("");
return Err(ApiError::MoltbookError(error.to_string(), hint.to_string()));
}
return Err(ApiError::MoltbookError(format!("HTTP {}", status), text));
}
serde_json::from_str(&text).map_err(ApiError::ParseError)
}
}