rover-api 0.1.0

Rust client library for the RoVer Bot Developer API
Documentation
use reqwest::{Client, Response};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use thiserror::Error;

pub mod types;
pub mod rate_limiter;
pub use types::*;
pub use rate_limiter::*;

const BASE_URL: &str = "https://registry.rover.link/api";

#[derive(Error, Debug)]
pub enum RoverApiError {
    #[error("HTTP request failed: {0}")]
    Http(#[from] reqwest::Error),
    
    #[error("JSON parsing failed: {0}")]
    Json(#[from] serde_json::Error),
    
    #[error("API error ({code}): {message}")]
    Api {
        code: String,
        message: String,
        detail: Option<String>,
        context: Option<serde_json::Value>,
    },
    
    #[error("Rate limit exceeded. Retry after {retry_after} seconds")]
    RateLimit { retry_after: u64 },
    
    #[error("Invalid API key")]
    InvalidApiKey,
}

pub struct RoverClient {
    client: Client,
    api_key: String,
    rate_limiter: Arc<Mutex<RateLimiter>>,
}

impl RoverClient {
    pub fn new(api_key: String) -> Self {
        Self {
            client: Client::new(),
            api_key,
            rate_limiter: Arc::new(Mutex::new(RateLimiter::new())),
        }
    }

    pub fn with_client(api_key: String, client: Client) -> Self {
        Self {
            client,
            api_key,
            rate_limiter: Arc::new(Mutex::new(RateLimiter::new())),
        }
    }

    async fn make_request<T>(&self, method: &str, endpoint: &str) -> Result<T, RoverApiError>
    where
        T: for<'de> Deserialize<'de>,
    {
        // Check rate limits before making request
        {
            let mut limiter = self.rate_limiter.lock().unwrap();
            limiter.check_rate_limit()?;
        }

        let url = format!("{}{}", BASE_URL, endpoint);
        let request = match method {
            "GET" => self.client.get(&url),
            "POST" => self.client.post(&url),
            "DELETE" => self.client.delete(&url),
            _ => return Err(RoverApiError::Api {
                code: "invalid_http_method".to_string(),
                message: "Invalid HTTP method".to_string(),
                detail: None,
                context: None,
            }),
        };

        let response = request
            .header("Authorization", format!("Bearer {}", self.api_key))
            .header("User-Agent", "rover-api-rs/0.1.0")
            .send()
            .await?;

        // Update rate limiter with response headers
        {
            let mut limiter = self.rate_limiter.lock().unwrap();
            limiter.update_from_headers(&response);
        }

        if response.status().is_success() {
            let data = response.json::<T>().await?;
            Ok(data)
        } else if response.status() == 429 {
            let retry_after = response
                .headers()
                .get("Retry-After")
                .and_then(|h| h.to_str().ok())
                .and_then(|s| s.parse::<u64>().ok())
                .unwrap_or(60);
            
            Err(RoverApiError::RateLimit { retry_after })
        } else if response.status() == 401 {
            Err(RoverApiError::InvalidApiKey)
        } else {
            let error_response: ApiErrorResponse = response.json().await?;
            Err(RoverApiError::Api {
                code: error_response.error_code,
                message: error_response.message,
                detail: error_response.detail,
                context: error_response.context,
            })
        }
    }

    /// Fetch a Discord member's verified Roblox account
    pub async fn get_discord_to_roblox(
        &self,
        guild_id: u64,
        user_id: u64,
    ) -> Result<DiscordToRobloxResponse, RoverApiError> {
        let endpoint = format!("/guilds/{}/discord-to-roblox/{}", guild_id, user_id);
        self.make_request("GET", &endpoint).await
    }

    /// Retrieve all Discord users in a guild associated with a specific Roblox ID
    pub async fn get_roblox_to_discord(
        &self,
        guild_id: u64,
        roblox_id: u64,
    ) -> Result<RobloxToDiscordResponse, RoverApiError> {
        let endpoint = format!("/guilds/{}/roblox-to-discord/{}", guild_id, roblox_id);
        self.make_request("GET", &endpoint).await
    }

    /// Trigger a manual user update (requires RoVer Plus)
    pub async fn update_user(
        &self,
        guild_id: u64,
        user_id: u64,
    ) -> Result<UpdateUserResponse, RoverApiError> {
        let endpoint = format!("/guilds/{}/update/{}", guild_id, user_id);
        self.make_request("POST", &endpoint).await
    }

    /// Revoke the current API key
    pub async fn revoke_api_key(&self) -> Result<(), RoverApiError> {
        let _: serde_json::Value = self.make_request("DELETE", "/api-key").await?;
        Ok(())
    }

    /// Get current rate limit status
    pub fn get_rate_limit_status(&self) -> RateLimitStatus {
        let limiter = self.rate_limiter.lock().unwrap();
        limiter.get_status()
    }
}