use crate::config;
use crate::error::{Error, Result};
use crate::rate_limiter;
use reqwest::{multipart::Part, Client};
use serde_json::Value;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
pub struct MaxClient {
pub(crate) http: Client,
pub(crate) 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 from_env() -> Result<Self> {
let token = std::env::var("MAXBOT_TOKEN").map_err(|e|
Error::InvalidInput(format!("MAXBOT_TOKEN not set: {}", e))
)?;
let mut client = Self::new(token);
if let Ok(base_url) = std::env::var("MAXBOT_PROXY") {
client.set_base_url(base_url);
}
Ok(client)
}
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().await;
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 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(),
))
}
pub(crate) async fn download_to_temp_dir(
&self,
url: &str,
desired_filename: &str,
) -> Result<PathBuf> {
let response = self.http.get(url).send().await?;
if !response.status().is_success() {
return Err(crate::error::Error::InvalidInput(format!(
"Failed to download from URL: HTTP {}",
response.status()
)));
}
let bytes = response.bytes().await?;
let temp_dir = std::env::temp_dir().join(format!("maxbot_{}", uuid::Uuid::new_v4()));
tokio::fs::create_dir(&temp_dir).await?;
let file_path = temp_dir.join(desired_filename);
tokio::fs::write(&file_path, bytes).await?;
Ok(file_path)
}
}