use reqwest::multipart::Part;
use reqwest::Client;
use serde_json::Value;
use std::time::Duration;
use crate::error::{Error, Result};
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::time::{self, Instant};
pub struct MaxClient {
http: Client,
token: String,
base_url: String,
last_request_time: Arc<Mutex<Instant>>,
}
const MIN_REQUEST_INTERVAL: Duration = Duration::from_millis(1000 / 30);
impl MaxClient {
pub fn new(token: impl Into<String>) -> Self {
Self {
http: Client::new(),
token: token.into(),
base_url: "https://platform-api.max.ru".to_string(),
last_request_time: Arc::new(Mutex::new(Instant::now() - MIN_REQUEST_INTERVAL)),
}
}
pub(crate) async fn request_with_rate_limit<T: serde::de::DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
query: &[(&str, String)],
body: Option<serde_json::Value>,
) -> Result<T> {
{
let mut last = self.last_request_time.lock().await;
let now = Instant::now();
let elapsed = now - *last;
if elapsed < MIN_REQUEST_INTERVAL {
time::sleep(MIN_REQUEST_INTERVAL - elapsed).await;
}
*last = Instant::now();
}
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()))
}
}