use crate::config;
use crate::error::{Error, Result};
use crate::rate_limiter;
use reqwest::{Client, multipart::Part};
use serde_json::Value;
use std::sync::Mutex;
use std::time::{Duration, Instant};
pub struct MaxClient {
http: Client,
token: String,
base_url: String, max_rps: Option<usize>, last_request_time: Option<Mutex<Instant>>, }
impl MaxClient {
pub fn new(token: impl Into<String>) -> Self {
Self {
http: Client::new(),
token: token.into(),
base_url: config::get_global_base_url(),
max_rps: None,
last_request_time: None,
}
}
pub fn set_base_url(&mut self, url: impl Into<String>) {
self.base_url = url.into();
}
pub fn set_max_rps(&mut self, rps: Option<usize>) {
self.max_rps = rps;
if rps.is_some() {
if self.last_request_time.is_none() {
self.last_request_time = Some(Mutex::new(Instant::now()));
}
} else {
self.last_request_time = None;
}
}
async fn apply_rate_limit(&self) {
if let Some(max_rps) = self.max_rps {
if max_rps > 0 {
let min_interval = Duration::from_millis(1000 / max_rps as u64);
let last = self.last_request_time.as_ref().unwrap();
let mut last_guard = last.lock().unwrap();
let now = Instant::now();
let elapsed = now - *last_guard;
if elapsed < min_interval {
tokio::time::sleep(min_interval - elapsed).await;
}
*last_guard = Instant::now();
}
} else {
rate_limiter::enforce_global_rate_limit().await;
}
}
pub(crate) async fn request_with_rate_limit<T: serde::de::DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
query: &[(&str, String)],
body: Option<Value>,
) -> Result<T> {
self.apply_rate_limit().await;
self.request(method, path, query, body).await
}
pub(crate) async fn request<T: serde::de::DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
query: &[(&str, String)],
body: Option<Value>,
) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let mut req = self.http.request(method, &url)
.header("Authorization", &self.token)
.header("Content-Type", "application/json");
if !query.is_empty() {
req = req.query(query);
}
if let Some(body) = body {
req = req.json(&body);
}
let resp = req.send().await?;
let status = resp.status();
let data: Value = resp.json().await?;
if status.is_success() {
serde_json::from_value(data).map_err(Error::from)
} else {
let message = data.get("message")
.and_then(|v| v.as_str())
.unwrap_or("unknown error");
Err(Error::Api { code: status.as_u16(), message: message.to_string() })
}
}
pub(crate) async fn upload_file(&self, file_path: &std::path::Path, file_type: &str) -> Result<String> {
let upload_info: Value = self.request(
reqwest::Method::POST,
"/uploads",
&[("type", file_type.to_string())],
None,
).await?;
let upload_url = upload_info["url"].as_str()
.ok_or_else(|| Error::InvalidInput("No upload URL in response".into()))?;
let token_from_upload = upload_info.get("token").and_then(|v| v.as_str()).map(String::from);
let file_content = tokio::fs::read(file_path).await?;
let file_name = file_path.file_name().unwrap().to_string_lossy().to_string();
let part = Part::bytes(file_content)
.file_name(file_name)
.mime_str("application/octet-stream")?;
let form = reqwest::multipart::Form::new().part("data", part);
let upload_resp = self.http.post(upload_url)
.header("Authorization", &self.token)
.multipart(form)
.send()
.await?;
let status = upload_resp.status();
if !status.is_success() {
let err_text = upload_resp.text().await.unwrap_or_default();
eprintln!("Upload failed with status {}: {}", status, err_text);
return Err(Error::Api {
code: status.as_u16(),
message: format!("Upload failed: {}", err_text),
});
}
if let Some(tok) = token_from_upload {
return Ok(tok);
}
let resp_json: Value = upload_resp.json().await?;
if let Some(tok) = resp_json["token"].as_str() {
return Ok(tok.to_string());
}
if let Some(photos) = resp_json["photos"].as_object() {
for photo in photos.values() {
if let Some(tok) = photo["token"].as_str() {
return Ok(tok.to_string());
}
}
}
Err(Error::InvalidInput("No token found in upload response".into()))
}
}