954 lines
30 KiB
Rust
954 lines
30 KiB
Rust
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<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct IsBotThereResponse {
|
|
pub results: Vec<bool>,
|
|
}
|
|
|
|
pub struct ApiState {
|
|
pub cache: Arc<Cache>,
|
|
pub db: Surreal<Client>,
|
|
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<D>(deserializer: D) -> Result<Self, D::Error>
|
|
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<E>(self, value: u64) -> Result<Self::Value, E>
|
|
where
|
|
E: serde::de::Error,
|
|
{
|
|
Ok(TrackLevelRole {
|
|
role_id: value.to_string(),
|
|
level: 0, // Default level for legacy data
|
|
})
|
|
}
|
|
|
|
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
|
where
|
|
E: serde::de::Error,
|
|
{
|
|
Ok(TrackLevelRole {
|
|
role_id: value.to_string(), // Convert to string
|
|
level: 0,
|
|
})
|
|
}
|
|
|
|
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
|
where
|
|
A: serde::de::MapAccess<'de>,
|
|
{
|
|
let mut role_id = None;
|
|
let mut level = None;
|
|
|
|
while let Some(key) = map.next_key::<String>()? {
|
|
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::<serde::de::IgnoredAny>()?;
|
|
}
|
|
}
|
|
}
|
|
|
|
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<std::collections::HashMap<String, Vec<TrackLevelRole>>>,
|
|
level_up_role_mapper: Option<std::collections::HashMap<String, u64>>,
|
|
}
|
|
|
|
#[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<S, B> Transform<S, ServiceRequest> for ApiKeyMiddleware
|
|
where
|
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = error::Error>,
|
|
S::Future: 'static,
|
|
B: 'static,
|
|
{
|
|
type Response = ServiceResponse<EitherBody<B>>;
|
|
type Error = error::Error;
|
|
type InitError = ();
|
|
type Transform = ApiKeyMiddlewareService<S>;
|
|
type Future = futures::future::Ready<Result<Self::Transform, Self::InitError>>;
|
|
|
|
fn new_transform(&self, service: S) -> Self::Future {
|
|
futures::future::ok(ApiKeyMiddlewareService {
|
|
service,
|
|
api_key: self.api_key.clone(),
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct ApiKeyMiddlewareService<S> {
|
|
service: S,
|
|
api_key: String,
|
|
}
|
|
|
|
impl<S, B> Service<ServiceRequest> for ApiKeyMiddlewareService<S>
|
|
where
|
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = error::Error>,
|
|
S::Future: 'static,
|
|
B: 'static,
|
|
{
|
|
type Response = ServiceResponse<EitherBody<B>>;
|
|
type Error = error::Error;
|
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
|
|
|
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<ServiceResponse<B>, error::Error>| async move {
|
|
Ok(res?.map_into_left_body())
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[post("/api/is_bot_there")]
|
|
async fn is_bot_there(
|
|
request_body: web::Json<IsBotThereRequest>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
info!(
|
|
"Processing /api/is_bot_there request with {} guild IDs",
|
|
request_body.guild_ids.len()
|
|
);
|
|
|
|
let results: Vec<bool> = request_body
|
|
.guild_ids
|
|
.iter()
|
|
.map(|guild_id_str| {
|
|
if let Ok(guild_id) = guild_id_str.parse::<u64>() {
|
|
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<u64>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<LeaderboardEntry> = 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::<LeaderboardMember>::new()));
|
|
}
|
|
|
|
info!("Found {} members in leaderboard for guild {}", entries.len(), guild_id_value);
|
|
|
|
// Fetch user data for all entries
|
|
let mut leaderboard: Vec<LeaderboardMember> = 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::<u64>().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<u64>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<Role> = 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<u64>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<GuildRecord> = 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<String, Vec<TrackLevelRole>> = 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<TrackLevelRole>,
|
|
}
|
|
|
|
#[actix_web::post("/api/{guild_id}/level/track")]
|
|
async fn create_level_track(
|
|
guild_id: web::Path<u64>,
|
|
body: web::Json<CreateLevelTrackRequest>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<GuildRecord> = 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<String, Vec<TrackLevelRole>>,
|
|
}
|
|
|
|
let _: Option<GuildUpdate> = 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<TrackLevelRole>,
|
|
}
|
|
|
|
#[actix_web::patch("/api/{guild_id}/level/track")]
|
|
async fn update_level_track(
|
|
guild_id: web::Path<u64>,
|
|
body: web::Json<UpdateLevelTrackRequest>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<GuildRecord> = 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<String, Vec<TrackLevelRole>>,
|
|
}
|
|
|
|
let _: Option<GuildUpdate> = 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<u64>,
|
|
body: web::Json<DeleteLevelTrackRequest>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<GuildRecord> = 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<String, Vec<TrackLevelRole>>,
|
|
}
|
|
|
|
let _: Option<GuildUpdate> = 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<u64>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<GuildRecord> = 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<LevelBridgerEntry> = 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<u64>,
|
|
body: web::Json<LevelBridgerEntry>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<GuildRecord> = 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::<u64>().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<String, u64>,
|
|
}
|
|
|
|
let _: Option<GuildUpdate> = 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<u64>,
|
|
body: web::Json<LevelBridgerEntry>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<GuildRecord> = 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::<u64>().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<String, u64>,
|
|
}
|
|
|
|
let _: Option<GuildUpdate> = 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<u64>,
|
|
body: web::Json<DeleteLevelBridgerRequest>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<GuildRecord> = 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<String, u64>,
|
|
}
|
|
|
|
let _: Option<GuildUpdate> = 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<chrono::Utc>,
|
|
pub added_by: String,
|
|
}
|
|
|
|
#[get("/api/beta_testing")]
|
|
async fn get_beta_servers(
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
info!("Processing GET /api/beta_testing request");
|
|
|
|
let servers: Vec<BetaServer> = 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<CreateBetaServerRequest>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<BetaServer> = 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<String>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
let guild_id_value = guild_id.into_inner();
|
|
info!("Processing DELETE /api/beta_testing/{} request", guild_id_value);
|
|
|
|
let _: Option<BetaServer> = 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<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct IsBetaServerResponse {
|
|
pub results: Vec<bool>,
|
|
}
|
|
|
|
#[post("/api/is_beta_server")]
|
|
async fn is_beta_server(
|
|
body: web::Json<IsBetaServerRequest>,
|
|
data: web::Data<ApiState>,
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
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<surrealdb::sql::Thing> = 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<BetaServer> = 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<String> = found_servers
|
|
.into_iter()
|
|
.map(|s| s.guild_id)
|
|
.collect();
|
|
|
|
let results: Vec<bool> = 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<Cache>,
|
|
db: Surreal<Client>,
|
|
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
|
|
}
|