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 {
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 {
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 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 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 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 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 pub fn get_rate_limit_status(&self) -> RateLimitStatus {
159 let limiter = self.rate_limiter.lock().unwrap();
160 limiter.get_status()
161 }
162}