maxbot 0.1.3

Автоматизация работы с чат-ботами MAX
Documentation
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};

/// Клиент для взаимодействия с API MAX.
pub struct MaxClient {
    http: Client,
    token: String,
    base_url: String,               // может быть переопределён для клиента
    max_rps: Option<usize>,         // если Some, то переопределяет глобальный
    last_request_time: Option<Mutex<Instant>>, // для локального rate limiter
}

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,
        }
    }

    /// Устанавливает базовый URL для данного клиента (переопределяет глобальный).
    pub fn set_base_url(&mut self, url: impl Into<String>) {
        self.base_url = url.into();
    }

    /// Устанавливает ограничение RPS для данного клиента.
    /// Если `rps` равно `None`, используется глобальное ограничение.
    /// Если `rps` равно `Some(0)`, ограничение отключается.
    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;
        }
    }

    /// Применяет rate limiting в соответствии с настройками клиента или глобальными.
    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
            rate_limiter::enforce_global_rate_limit().await;
        }
    }

    /// Выполняет HTTP-запрос с соблюдением rate limiting.
    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
    }

    /// Выполняет HTTP-запрос без rate limiting (используется внутри).
    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() })
        }
    }

    /// Загружает файл на сервер MAX и возвращает токен вложения.
    pub(crate) async fn upload_file(&self, file_path: &std::path::Path, file_type: &str) -> Result<String> {
        // 1. Получить URL для загрузки (и, возможно, токен)
        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);

        // 2. Прочитать файл
        let file_content = tokio::fs::read(file_path).await?;
        let file_name = file_path.file_name().unwrap().to_string_lossy().to_string();

        // 3. Выполнить multipart-загрузку
        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),
            });
        }

        // 4. Извлечь токен
        if let Some(tok) = token_from_upload {
            // Для видео/аудио токен уже был в первом ответе
            return Ok(tok);
        }

        // Для image/file парсим JSON-ответ
        let resp_json: Value = upload_resp.json().await?;
        // Ищем токен в поле "token" (может быть в корне)
        if let Some(tok) = resp_json["token"].as_str() {
            return Ok(tok.to_string());
        }
        // Или в "photos" (для изображений)
        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()))
    }
}