Added endpoints for level and leaderboard
This commit is contained in:
502
src/api.rs
502
src/api.rs
@@ -38,17 +38,94 @@ pub struct LeaderboardMember {
|
||||
pub struct Role {
|
||||
pub role_id: String,
|
||||
pub role_name: String,
|
||||
pub color: u32,
|
||||
pub position: u16,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LevelRole {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct TrackLevelRole {
|
||||
pub role_id: u64,
|
||||
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,
|
||||
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 as u64,
|
||||
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"));
|
||||
}
|
||||
role_id = Some(map.next_value()?);
|
||||
}
|
||||
"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<u64>>>,
|
||||
level_role_stack: Option<std::collections::HashMap<String, Vec<TrackLevelRole>>>,
|
||||
level_up_role_mapper: Option<std::collections::HashMap<String, u64>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -275,6 +352,8 @@ async fn get_roles(
|
||||
.map(|(role_id, role)| Role {
|
||||
role_id: role_id.to_string(),
|
||||
role_name: role.name.clone(),
|
||||
color: role.colour.0,
|
||||
position: role.position,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -288,14 +367,13 @@ async fn get_level_tracks(
|
||||
data: web::Data<ApiState>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let guild_id_value = guild_id.into_inner();
|
||||
info!("Processing /api/{}/level/track request", guild_id_value);
|
||||
info!("Processing GET /api/{}/level/track 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")
|
||||
})?;
|
||||
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
|
||||
@@ -304,24 +382,14 @@ async fn get_level_tracks(
|
||||
error::ErrorInternalServerError("Database query failed")
|
||||
})?;
|
||||
|
||||
let mut result: std::collections::HashMap<String, Vec<LevelRole>> = std::collections::HashMap::new();
|
||||
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, role_ids) in level_role_stack {
|
||||
let mut roles: Vec<LevelRole> = Vec::new();
|
||||
for role_id in role_ids {
|
||||
let role_id_obj = serenity::all::RoleId::new(role_id);
|
||||
let role_name = guild.roles.get(&role_id_obj)
|
||||
.map(|r| r.name.clone())
|
||||
.unwrap_or_else(|| "Unknown Role".to_string());
|
||||
|
||||
roles.push(LevelRole {
|
||||
id: role_id.to_string(),
|
||||
name: role_name,
|
||||
});
|
||||
}
|
||||
result.insert(track_name, roles);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -330,6 +398,381 @@ async fn get_level_tracks(
|
||||
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"
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn start_api_server(
|
||||
cache: Arc<Cache>,
|
||||
db: Surreal<Client>,
|
||||
@@ -348,6 +791,13 @@ pub async fn start_api_server(
|
||||
.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)
|
||||
})
|
||||
.bind(("0.0.0.0", port))?
|
||||
.run()
|
||||
|
||||
Reference in New Issue
Block a user