use actix_web::{web, App, HttpServer, HttpResponse, post, get, delete, error, body::EitherBody, dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}}; use futures::future::LocalBoxFuture; use futures::FutureExt; use serde::{Deserialize, Serialize}; use serenity::cache::Cache; use std::sync::Arc; use surrealdb::Surreal; use surrealdb::engine::remote::ws::Client; use tracing::{info, warn}; use chrono; #[derive(Deserialize)] pub struct IsBotThereRequest { pub guild_ids: Vec, } #[derive(Serialize)] pub struct IsBotThereResponse { pub results: Vec, } pub struct ApiState { pub cache: Arc, pub db: Surreal, pub api_key: String, } #[derive(Serialize)] pub struct LeaderboardMember { pub user_id: String, pub username: String, pub avatar: String, pub level: u64, pub xp: u64, pub rank: usize, } #[derive(Serialize)] pub struct Role { pub role_id: String, pub role_name: String, pub color: u32, pub position: u16, } #[derive(Serialize, Clone, Debug)] pub struct TrackLevelRole { pub role_id: String, pub level: u64, } impl<'de> Deserialize<'de> for TrackLevelRole { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct TrackLevelRoleVisitor; impl<'de> serde::de::Visitor<'de> for TrackLevelRoleVisitor { type Value = TrackLevelRole; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a u64 (legacy) or a TrackLevelRole struct") } fn visit_u64(self, value: u64) -> Result where E: serde::de::Error, { Ok(TrackLevelRole { role_id: value.to_string(), level: 0, // Default level for legacy data }) } fn visit_i64(self, value: i64) -> Result where E: serde::de::Error, { Ok(TrackLevelRole { role_id: value.to_string(), // Convert to string level: 0, }) } fn visit_map(self, mut map: A) -> Result where A: serde::de::MapAccess<'de>, { let mut role_id = None; let mut level = None; while let Some(key) = map.next_key::()? { match key.as_str() { "role_id" => { if role_id.is_some() { return Err(serde::de::Error::duplicate_field("role_id")); } let v: serde_json::Value = map.next_value()?; if let Some(s) = v.as_str() { role_id = Some(s.to_string()); } else if let Some(n) = v.as_u64() { role_id = Some(n.to_string()); } else { return Err(serde::de::Error::custom("role_id must be string or number")); } } "level" => { if level.is_some() { return Err(serde::de::Error::duplicate_field("level")); } level = Some(map.next_value()?); } _ => { let _ = map.next_value::()?; } } } let role_id = role_id.ok_or_else(|| serde::de::Error::missing_field("role_id"))?; let level = level.ok_or_else(|| serde::de::Error::missing_field("level"))?; Ok(TrackLevelRole { role_id, level }) } } deserializer.deserialize_any(TrackLevelRoleVisitor) } } #[derive(Deserialize)] struct GuildRecord { level_role_stack: Option>>, level_up_role_mapper: Option>, } #[derive(Deserialize, Debug)] struct LeaderboardEntry { id: surrealdb::sql::Thing, xp: u64, level: u64, } /// API Key Authentication Middleware pub struct ApiKeyMiddleware { api_key: String, } impl ApiKeyMiddleware { pub fn new(api_key: String) -> Self { ApiKeyMiddleware { api_key } } } impl Transform for ApiKeyMiddleware where S: Service, Error = error::Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse>; type Error = error::Error; type InitError = (); type Transform = ApiKeyMiddlewareService; type Future = futures::future::Ready>; fn new_transform(&self, service: S) -> Self::Future { futures::future::ok(ApiKeyMiddlewareService { service, api_key: self.api_key.clone(), }) } } pub struct ApiKeyMiddlewareService { service: S, api_key: String, } impl Service for ApiKeyMiddlewareService where S: Service, Error = error::Error>, S::Future: 'static, B: 'static, { type Response = ServiceResponse>; type Error = error::Error; type Future = LocalBoxFuture<'static, Result>; forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { let api_key = self.api_key.clone(); let path = req.path().to_string(); // Check API key from header let header_key = req .headers() .get("X-API-Key") .and_then(|h| h.to_str().ok()) .unwrap_or("") .to_string(); if header_key.is_empty() || header_key != api_key { warn!("Unauthorized API request to {} - missing or invalid API key", path); return Box::pin(async move { Err(error::ErrorUnauthorized("Missing or invalid API key")) }); } info!("Authorized API request to {}", path); Box::pin( self.service .call(req) .then(|res: Result, error::Error>| async move { Ok(res?.map_into_left_body()) }), ) } } #[post("/api/is_bot_there")] async fn is_bot_there( request_body: web::Json, data: web::Data, ) -> Result { info!( "Processing /api/is_bot_there request with {} guild IDs", request_body.guild_ids.len() ); let results: Vec = request_body .guild_ids .iter() .map(|guild_id_str| { if let Ok(guild_id) = guild_id_str.parse::() { let exists = data.cache.guild(guild_id).is_some(); if !exists { info!("Bot cache miss for guild {}", guild_id); } exists } else { warn!("Invalid guild ID string: {}", guild_id_str); false } }) .collect(); let found_count = results.iter().filter(|&&b| b).count(); info!( "Bot found in {}/{} requested guilds", found_count, results.len() ); Ok(HttpResponse::Ok().json(IsBotThereResponse { results })) } #[get("/api/{guild_id}/leaderboard")] async fn get_leaderboard( guild_id: web::Path, data: web::Data, ) -> Result { let guild_id_value = guild_id.into_inner(); info!("Processing /api/{}/leaderboard request", guild_id_value); // Check if bot is in the guild if data.cache.guild(guild_id_value).is_none() { warn!("Bot is not in guild {}", guild_id_value); return Err(error::ErrorNotFound("Bot is not in this guild")); } // Query all users for this guild, ordered by level and xp let sql = "SELECT * FROM levels WHERE string::starts_with(record::id(id), $prefix) ORDER BY level DESC, xp DESC"; let prefix = format!("{}:", guild_id_value); let mut response = data.db.query(sql).bind(("prefix", prefix)).await .map_err(|e| { warn!("Database query error: {}", e); error::ErrorInternalServerError("Database query failed") })?; let entries: Vec = response.take(0) .map_err(|e| { warn!("Failed to parse database response: {}", e); error::ErrorInternalServerError("Failed to parse database response") })?; if entries.is_empty() { info!("No leaderboard data found for guild {}", guild_id_value); return Ok(HttpResponse::Ok().json(Vec::::new())); } info!("Found {} members in leaderboard for guild {}", entries.len(), guild_id_value); // Fetch user data for all entries let mut leaderboard: Vec = Vec::new(); for (i, entry) in entries.iter().enumerate() { // Extract user id from Surreal Thing let id_value = &entry.id.id; let clean_id_str = match id_value { surrealdb::sql::Id::String(s) => s.as_str().to_string(), _ => entry.id.id.to_string(), }; let parts: Vec<&str> = clean_id_str.split(':').collect(); let user_id_str = parts.last().unwrap_or(&"0"); let user_id_u64 = user_id_str.parse::().unwrap_or(0); if user_id_u64 == 0 { continue; } let mut username = String::from("Unknown User"); let mut avatar_url = String::new(); let user_id = serenity::all::UserId::new(user_id_u64); // Try to get member info from cache first if let Some(guild) = data.cache.guild(guild_id_value) { if let Some(member) = guild.members.get(&user_id) { username = member.display_name().to_string(); avatar_url = member.user.face(); } } // If not in cache, we'll just use Unknown User if avatar_url.is_empty() { info!("User {} not in cache for guild {}", user_id_u64, guild_id_value); } leaderboard.push(LeaderboardMember { user_id: user_id_u64.to_string(), username, avatar: avatar_url, level: entry.level, xp: entry.xp, rank: i + 1, }); } info!("Returning {} members in leaderboard", leaderboard.len()); Ok(HttpResponse::Ok().json(leaderboard)) } #[get("/api/{guild_id}/roles")] async fn get_roles( guild_id: web::Path, data: web::Data, ) -> Result { let guild_id_value = guild_id.into_inner(); info!("Processing /api/{}/roles request", guild_id_value); // Check if bot is in the guild let guild = data.cache.guild(guild_id_value) .ok_or_else(|| { warn!("Bot is not in guild {}", guild_id_value); error::ErrorNotFound("Bot is not in this guild") })?; // Get all roles from the guild let roles: Vec = guild.roles .iter() .map(|(role_id, role)| Role { role_id: role_id.to_string(), role_name: role.name.clone(), color: role.colour.0, position: role.position, }) .collect(); info!("Returning {} roles for guild {}", roles.len(), guild_id_value); Ok(HttpResponse::Ok().json(roles)) } #[get("/api/{guild_id}/level/track")] async fn get_level_tracks( guild_id: web::Path, data: web::Data, ) -> Result { let guild_id_value = guild_id.into_inner(); info!("Processing GET /api/{}/level/track request", guild_id_value); // Check if bot is in the guild if data.cache.guild(guild_id_value).is_none() { warn!("Bot is not in guild {}", guild_id_value); return Err(error::ErrorNotFound("Bot is not in this guild")); } // Query the guild record from database let record: Option = data.db.select(("guilds", guild_id_value.to_string())).await .map_err(|e| { warn!("Database query error: {}", e); error::ErrorInternalServerError("Database query failed") })?; let mut result: std::collections::HashMap> = std::collections::HashMap::new(); if let Some(record) = record { if let Some(level_role_stack) = record.level_role_stack { for (track_name, mut track_roles) in level_role_stack { // Sort by level ascending track_roles.sort_by_key(|r| r.level); result.insert(track_name, track_roles); } } } info!("Returning {} level tracks for guild {}", result.len(), guild_id_value); Ok(HttpResponse::Ok().json(result)) } #[derive(Deserialize)] pub struct CreateLevelTrackRequest { pub track_name: String, pub roles: Vec, } #[actix_web::post("/api/{guild_id}/level/track")] async fn create_level_track( guild_id: web::Path, body: web::Json, data: web::Data, ) -> Result { let guild_id_value = guild_id.into_inner(); info!("Processing POST /api/{}/level/track request", guild_id_value); // Check if bot is in the guild if data.cache.guild(guild_id_value).is_none() { warn!("Bot is not in guild {}", guild_id_value); return Err(error::ErrorNotFound("Bot is not in this guild")); } // Get existing record let record: Option = data.db.select(("guilds", guild_id_value.to_string())).await .map_err(|e| { warn!("Database query error: {}", e); error::ErrorInternalServerError("Database query failed") })?; let mut level_role_stack = record .and_then(|r| r.level_role_stack) .unwrap_or_default(); // Check if track already exists if level_role_stack.contains_key(&body.track_name) { return Err(error::ErrorConflict("Track already exists. Use PATCH to update.")); } // Add new track level_role_stack.insert(body.track_name.clone(), body.roles.clone()); // Update the database #[derive(Serialize, Deserialize)] struct GuildUpdate { level_role_stack: std::collections::HashMap>, } let _: Option = data.db .upsert(("guilds", guild_id_value.to_string())) .merge(GuildUpdate { level_role_stack }) .await .map_err(|e| { warn!("Database update error: {}", e); error::ErrorInternalServerError("Database update failed") })?; info!("Created level track '{}' for guild {}", body.track_name, guild_id_value); Ok(HttpResponse::Created().json(serde_json::json!({ "success": true, "message": format!("Level track '{}' created successfully", body.track_name) }))) } #[derive(Deserialize)] pub struct UpdateLevelTrackRequest { pub track_name: String, pub roles: Vec, } #[actix_web::patch("/api/{guild_id}/level/track")] async fn update_level_track( guild_id: web::Path, body: web::Json, data: web::Data, ) -> Result { let guild_id_value = guild_id.into_inner(); info!("Processing PATCH /api/{}/level/track request", guild_id_value); // Check if bot is in the guild if data.cache.guild(guild_id_value).is_none() { warn!("Bot is not in guild {}", guild_id_value); return Err(error::ErrorNotFound("Bot is not in this guild")); } // Get existing record let record: Option = data.db.select(("guilds", guild_id_value.to_string())).await .map_err(|e| { warn!("Database query error: {}", e); error::ErrorInternalServerError("Database query failed") })?; let mut level_role_stack = record .and_then(|r| r.level_role_stack) .unwrap_or_default(); // Update track (create if doesn't exist) level_role_stack.insert(body.track_name.clone(), body.roles.clone()); // Update the database #[derive(Serialize, Deserialize)] struct GuildUpdate { level_role_stack: std::collections::HashMap>, } let _: Option = data.db .upsert(("guilds", guild_id_value.to_string())) .merge(GuildUpdate { level_role_stack }) .await .map_err(|e| { warn!("Database update error: {}", e); error::ErrorInternalServerError("Database update failed") })?; info!("Updated level track '{}' for guild {}", body.track_name, guild_id_value); Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "message": format!("Level track '{}' updated successfully", body.track_name) }))) } #[derive(Deserialize)] pub struct DeleteLevelTrackRequest { pub track_name: String, } #[actix_web::delete("/api/{guild_id}/level/track")] async fn delete_level_track( guild_id: web::Path, body: web::Json, data: web::Data, ) -> Result { let guild_id_value = guild_id.into_inner(); info!("Processing DELETE /api/{}/level/track request", guild_id_value); // Check if bot is in the guild if data.cache.guild(guild_id_value).is_none() { warn!("Bot is not in guild {}", guild_id_value); return Err(error::ErrorNotFound("Bot is not in this guild")); } // Get existing record let record: Option = data.db.select(("guilds", guild_id_value.to_string())).await .map_err(|e| { warn!("Database query error: {}", e); error::ErrorInternalServerError("Database query failed") })?; let mut level_role_stack = record .and_then(|r| r.level_role_stack) .unwrap_or_default(); // Check if track exists if !level_role_stack.contains_key(&body.track_name) { return Err(error::ErrorNotFound("Track not found")); } // Remove track level_role_stack.remove(&body.track_name); // Update the database #[derive(Serialize, Deserialize)] struct GuildUpdate { level_role_stack: std::collections::HashMap>, } let _: Option = data.db .upsert(("guilds", guild_id_value.to_string())) .merge(GuildUpdate { level_role_stack }) .await .map_err(|e| { warn!("Database update error: {}", e); error::ErrorInternalServerError("Database update failed") })?; info!("Deleted level track '{}' for guild {}", body.track_name, guild_id_value); Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "message": format!("Level track '{}' deleted successfully", body.track_name) }))) } #[derive(Serialize, Deserialize)] pub struct LevelBridgerEntry { pub in_role_id: String, pub out_role_id: String, } #[get("/api/{guild_id}/level_bridger")] async fn get_level_bridger( guild_id: web::Path, data: web::Data, ) -> Result { let guild_id_value = guild_id.into_inner(); info!("Processing GET /api/{}/level_bridger request", guild_id_value); // Check if bot is in the guild if data.cache.guild(guild_id_value).is_none() { return Err(error::ErrorNotFound("Bot is not in this guild")); } let record: Option = data.db.select(("guilds", guild_id_value.to_string())).await .map_err(|e| { warn!("Database query error: {}", e); error::ErrorInternalServerError("Database query failed") })?; let mapper = record .and_then(|r| r.level_up_role_mapper) .unwrap_or_default(); let result: Vec = mapper .into_iter() .map(|(k, v)| LevelBridgerEntry { in_role_id: k, out_role_id: v.to_string(), }) .collect(); Ok(HttpResponse::Ok().json(result)) } #[post("/api/{guild_id}/level_bridger")] async fn create_level_bridger( guild_id: web::Path, body: web::Json, data: web::Data, ) -> Result { let guild_id_value = guild_id.into_inner(); info!("Processing POST /api/{}/level_bridger request", guild_id_value); if data.cache.guild(guild_id_value).is_none() { return Err(error::ErrorNotFound("Bot is not in this guild")); } let record: Option = data.db.select(("guilds", guild_id_value.to_string())).await .map_err(|e| { warn!("Database query error: {}", e); error::ErrorInternalServerError("Database query failed") })?; let mut mapper = record .and_then(|r| r.level_up_role_mapper) .unwrap_or_default(); if mapper.contains_key(&body.in_role_id) { return Err(error::ErrorConflict("Bridge already exists for this role. Use PUT to update.")); } // Validate roles exist? Optional but good practice. For now simpler is better unless requested. let out_role_u64 = body.out_role_id.parse::().map_err(|_| error::ErrorBadRequest("Invalid out_role_id"))?; mapper.insert(body.in_role_id.clone(), out_role_u64); #[derive(Serialize, Deserialize)] struct GuildUpdate { level_up_role_mapper: std::collections::HashMap, } let _: Option = data.db .upsert(("guilds", guild_id_value.to_string())) .merge(GuildUpdate { level_up_role_mapper: mapper }) .await .map_err(|e| { warn!("Database update error: {}", e); error::ErrorInternalServerError("Database update failed") })?; Ok(HttpResponse::Created().json(serde_json::json!({ "success": true, "message": "Level bridge created successfully" }))) } #[actix_web::put("/api/{guild_id}/level_bridger")] async fn update_level_bridger( guild_id: web::Path, body: web::Json, data: web::Data, ) -> Result { let guild_id_value = guild_id.into_inner(); info!("Processing PUT /api/{}/level_bridger request", guild_id_value); if data.cache.guild(guild_id_value).is_none() { return Err(error::ErrorNotFound("Bot is not in this guild")); } let record: Option = data.db.select(("guilds", guild_id_value.to_string())).await .map_err(|e| { warn!("Database query error: {}", e); error::ErrorInternalServerError("Database query failed") })?; let mut mapper = record .and_then(|r| r.level_up_role_mapper) .unwrap_or_default(); // Resetting or creating if not exists? User said PUT endpoints. Conventionally PUT replaces. // If it doesn't exist, we can create it too (upsert). let out_role_u64 = body.out_role_id.parse::().map_err(|_| error::ErrorBadRequest("Invalid out_role_id"))?; mapper.insert(body.in_role_id.clone(), out_role_u64); #[derive(Serialize, Deserialize)] struct GuildUpdate { level_up_role_mapper: std::collections::HashMap, } let _: Option = data.db .upsert(("guilds", guild_id_value.to_string())) .merge(GuildUpdate { level_up_role_mapper: mapper }) .await .map_err(|e| { warn!("Database update error: {}", e); error::ErrorInternalServerError("Database update failed") })?; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "message": "Level bridge updated successfully" }))) } #[derive(Deserialize)] pub struct DeleteLevelBridgerRequest { pub in_role_id: String, } #[actix_web::delete("/api/{guild_id}/level_bridger")] async fn delete_level_bridger( guild_id: web::Path, body: web::Json, data: web::Data, ) -> Result { let guild_id_value = guild_id.into_inner(); info!("Processing DELETE /api/{}/level_bridger request", guild_id_value); if data.cache.guild(guild_id_value).is_none() { return Err(error::ErrorNotFound("Bot is not in this guild")); } let record: Option = data.db.select(("guilds", guild_id_value.to_string())).await .map_err(|e| { warn!("Database query error: {}", e); error::ErrorInternalServerError("Database query failed") })?; let mut mapper = record .and_then(|r| r.level_up_role_mapper) .unwrap_or_default(); if !mapper.contains_key(&body.in_role_id) { return Err(error::ErrorNotFound("Bridge not found")); } mapper.remove(&body.in_role_id); #[derive(Serialize, Deserialize)] struct GuildUpdate { level_up_role_mapper: std::collections::HashMap, } let _: Option = data.db .upsert(("guilds", guild_id_value.to_string())) .merge(GuildUpdate { level_up_role_mapper: mapper }) .await .map_err(|e| { warn!("Database update error: {}", e); error::ErrorInternalServerError("Database update failed") })?; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "message": "Level bridge deleted successfully" }))) } #[derive(Serialize, Deserialize, Debug)] pub struct BetaServer { pub guild_id: String, pub added_at: chrono::DateTime, pub added_by: String, } #[get("/api/beta_testing")] async fn get_beta_servers( data: web::Data, ) -> Result { info!("Processing GET /api/beta_testing request"); let servers: Vec = data.db.select("beta_testing").await .map_err(|e| { warn!("Database query error: {}", e); error::ErrorInternalServerError("Database query failed") })?; Ok(HttpResponse::Ok().json(servers)) } #[derive(Deserialize)] pub struct CreateBetaServerRequest { pub guild_id: String, } #[post("/api/beta_testing")] async fn create_beta_server( body: web::Json, data: web::Data, ) -> Result { info!("Processing POST /api/beta_testing request for {}", body.guild_id); let beta_info = BetaServer { guild_id: body.guild_id.clone(), added_at: chrono::Utc::now(), added_by: "API".to_string(), }; let _: Option = data.db .create(("beta_testing", &body.guild_id)) .content(beta_info) .await .map_err(|e| { warn!("Database create error: {}", e); error::ErrorInternalServerError("Database create failed") })?; Ok(HttpResponse::Created().json(serde_json::json!({ "success": true, "message": format!("Server {} added to beta testing", body.guild_id) }))) } #[delete("/api/beta_testing/{guild_id}")] async fn delete_beta_server( guild_id: web::Path, data: web::Data, ) -> Result { let guild_id_value = guild_id.into_inner(); info!("Processing DELETE /api/beta_testing/{} request", guild_id_value); let _: Option = data.db .delete(("beta_testing", &guild_id_value)) .await .map_err(|e| { warn!("Database delete error: {}", e); error::ErrorInternalServerError("Database delete failed") })?; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "message": format!("Server {} removed from beta testing", guild_id_value) }))) } #[derive(Deserialize)] pub struct IsBetaServerRequest { pub guild_ids: Vec, } #[derive(Serialize)] pub struct IsBetaServerResponse { pub results: Vec, } #[post("/api/is_beta_server")] async fn is_beta_server( body: web::Json, data: web::Data, ) -> Result { info!("Processing POST /api/is_beta_server with {} IDs", body.guild_ids.len()); // Optimization: Single query to check all IDs let ids_to_check: Vec = body.guild_ids .iter() .map(|gid| surrealdb::sql::Thing::from(("beta_testing".to_string(), gid.to_string()))) .collect(); let sql = "SELECT * FROM beta_testing WHERE id IN $ids"; let mut response = data.db.query(sql) .bind(("ids", ids_to_check)) .await .map_err(|e| { warn!("Database query error: {}", e); error::ErrorInternalServerError("Database query failed") })?; let found_servers: Vec = response.take(0) .map_err(|e| { warn!("Failed to parse database response: {}", e); error::ErrorInternalServerError("Failed to parse database response") })?; let found_ids: std::collections::HashSet = found_servers .into_iter() .map(|s| s.guild_id) .collect(); let results: Vec = body.guild_ids .iter() .map(|gid| found_ids.contains(&gid.to_string())) .collect(); Ok(HttpResponse::Ok().json(IsBetaServerResponse { results })) } pub async fn start_api_server( cache: Arc, db: Surreal, api_key: String, port: u16, ) -> std::io::Result<()> { info!("Starting API server on port {}", port); let state = web::Data::new(ApiState { cache, db, api_key: api_key.clone() }); HttpServer::new(move || { App::new() .app_data(state.clone()) .wrap(ApiKeyMiddleware::new(api_key.clone())) .service(is_bot_there) .service(get_leaderboard) .service(get_roles) .service(get_level_tracks) .service(create_level_track) .service(update_level_track) .service(delete_level_track) .service(get_level_bridger) .service(create_level_bridger) .service(update_level_bridger) .service(delete_level_bridger) .service(get_beta_servers) .service(create_beta_server) .service(delete_beta_server) .service(is_beta_server) }) .bind(("0.0.0.0", port))? .run() .await }