Skip to main content

rover_api/
lib.rs

1use reqwest::{Client, Response};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, Instant};
6use thiserror::Error;
7
8pub mod types;
9pub mod rate_limiter;
10pub use types::*;
11pub use rate_limiter::*;
12
13const BASE_URL: &str = "https://registry.rover.link/api";
14
15#[derive(Error, Debug)]
16pub enum RoverApiError {
17    #[error("HTTP request failed: {0}")]
18    Http(#[from] reqwest::Error),
19    
20    #[error("JSON parsing failed: {0}")]
21    Json(#[from] serde_json::Error),
22    
23    #[error("API error ({code}): {message}")]
24    Api {
25        code: String,
26        message: String,
27        detail: Option<String>,
28        context: Option<serde_json::Value>,
29    },
30    
31    #[error("Rate limit exceeded. Retry after {retry_after} seconds")]
32    RateLimit { retry_after: u64 },
33    
34    #[error("Invalid API key")]
35    InvalidApiKey,
36}
37
38pub struct RoverClient {
39    client: Client,
40    api_key: String,
41    rate_limiter: Arc<Mutex<RateLimiter>>,
42}
43
44impl RoverClient {
45    pub fn new(api_key: String) -> Self {
46        Self {
47            client: Client::new(),
48            api_key,
49            rate_limiter: Arc::new(Mutex::new(RateLimiter::new())),
50        }
51    }
52
53    pub fn with_client(api_key: String, client: Client) -> Self {
54        Self {
55            client,
56            api_key,
57            rate_limiter: Arc::new(Mutex::new(RateLimiter::new())),
58        }
59    }
60
61    async fn make_request<T>(&self, method: &str, endpoint: &str) -> Result<T, RoverApiError>
62    where
63        T: for<'de> Deserialize<'de>,
64    {
65        // Check rate limits before making request
66        {
67            let mut limiter = self.rate_limiter.lock().unwrap();
68            limiter.check_rate_limit()?;
69        }
70
71        let url = format!("{}{}", BASE_URL, endpoint);
72        let request = match method {
73            "GET" => self.client.get(&url),
74            "POST" => self.client.post(&url),
75            "DELETE" => self.client.delete(&url),
76            _ => return Err(RoverApiError::Api {
77                code: "invalid_http_method".to_string(),
78                message: "Invalid HTTP method".to_string(),
79                detail: None,
80                context: None,
81            }),
82        };
83
84        let response = request
85            .header("Authorization", format!("Bearer {}", self.api_key))
86            .header("User-Agent", "rover-api-rs/0.1.0")
87            .send()
88            .await?;
89
90        // Update rate limiter with response headers
91        {
92            let mut limiter = self.rate_limiter.lock().unwrap();
93            limiter.update_from_headers(&response);
94        }
95
96        if response.status().is_success() {
97            let data = response.json::<T>().await?;
98            Ok(data)
99        } else if response.status() == 429 {
100            let retry_after = response
101                .headers()
102                .get("Retry-After")
103                .and_then(|h| h.to_str().ok())
104                .and_then(|s| s.parse::<u64>().ok())
105                .unwrap_or(60);
106            
107            Err(RoverApiError::RateLimit { retry_after })
108        } else if response.status() == 401 {
109            Err(RoverApiError::InvalidApiKey)
110        } else {
111            let error_response: ApiErrorResponse = response.json().await?;
112            Err(RoverApiError::Api {
113                code: error_response.error_code,
114                message: error_response.message,
115                detail: error_response.detail,
116                context: error_response.context,
117            })
118        }
119    }
120
121    /// Fetch a Discord member's verified Roblox account
122    pub async fn get_discord_to_roblox(
123        &self,
124        guild_id: u64,
125        user_id: u64,
126    ) -> Result<DiscordToRobloxResponse, RoverApiError> {
127        let endpoint = format!("/guilds/{}/discord-to-roblox/{}", guild_id, user_id);
128        self.make_request("GET", &endpoint).await
129    }
130
131    /// Retrieve all Discord users in a guild associated with a specific Roblox ID
132    pub async fn get_roblox_to_discord(
133        &self,
134        guild_id: u64,
135        roblox_id: u64,
136    ) -> Result<RobloxToDiscordResponse, RoverApiError> {
137        let endpoint = format!("/guilds/{}/roblox-to-discord/{}", guild_id, roblox_id);
138        self.make_request("GET", &endpoint).await
139    }
140
141    /// Trigger a manual user update (requires RoVer Plus)
142    pub async fn update_user(
143        &self,
144        guild_id: u64,
145        user_id: u64,
146    ) -> Result<UpdateUserResponse, RoverApiError> {
147        let endpoint = format!("/guilds/{}/update/{}", guild_id, user_id);
148        self.make_request("POST", &endpoint).await
149    }
150
151    /// Revoke the current API key
152    pub async fn revoke_api_key(&self) -> Result<(), RoverApiError> {
153        let _: serde_json::Value = self.make_request("DELETE", "/api-key").await?;
154        Ok(())
155    }
156
157    /// Get current rate limit status
158    pub fn get_rate_limit_status(&self) -> RateLimitStatus {
159        let limiter = self.rate_limiter.lock().unwrap();
160        limiter.get_status()
161    }
162}