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>,
{
{
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?;
{
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,
})
}
}
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
}
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
}
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
}
pub async fn revoke_api_key(&self) -> Result<(), RoverApiError> {
let _: serde_json::Value = self.make_request("DELETE", "/api-key").await?;
Ok(())
}
pub fn get_rate_limit_status(&self) -> RateLimitStatus {
let limiter = self.rate_limiter.lock().unwrap();
limiter.get_status()
}
}