diff --git a/src/api.rs b/src/api.rs index 7dbcb30..66cea5e 100644 --- a/src/api.rs +++ b/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(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, + level: 0, // Default level for legacy data + }) + } + + fn visit_i64(self, value: i64) -> Result + where + E: serde::de::Error, + { + Ok(TrackLevelRole { + role_id: value as u64, + 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")); + } + 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::()?; + } + } + } + + 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_role_stack: Option>>, + level_up_role_mapper: Option>, } #[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, ) -> Result { 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 = 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> = std::collections::HashMap::new(); + 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, role_ids) in level_role_stack { - let mut roles: Vec = 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, +} + +#[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" + }))) +} + pub async fn start_api_server( cache: Arc, db: Surreal, @@ -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() diff --git a/src/commands/level.rs b/src/commands/level.rs index 1150650..83ccc2a 100644 --- a/src/commands/level.rs +++ b/src/commands/level.rs @@ -7,7 +7,6 @@ use poise::serenity_prelude as serenity; use serde::Deserialize; use serenity::Mentionable; use std::collections::HashMap; -use std::io::Cursor; use std::sync::{Arc, OnceLock}; use std::time::Duration; @@ -52,9 +51,88 @@ pub struct UserLevel { pub last_message: chrono::DateTime, } +/// Role with level requirement - used for level track system +#[derive(serde::Serialize, Clone, Debug)] +pub struct TrackLevelRole { + pub role_id: u64, + 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, + level: 0, // Default level for legacy data + }) + } + + fn visit_i64(self, value: i64) -> Result + where + E: serde::de::Error, + { + Ok(TrackLevelRole { + role_id: value as u64, + 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")); + } + 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::()?; + } + } + } + + 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_role_stack: Option>>, levelup_channel: Option, levelup_message: Option, level_up_role_mapper: Option>, @@ -67,122 +145,6 @@ struct LeaderboardEntry { level: u64, } -#[poise::command( - slash_command, - prefix_command, - guild_only, - default_member_permissions = "MANAGE_GUILD", - required_permissions = "MANAGE_GUILD" -)] -pub async fn set_level_roles( - ctx: Context<'_>, - #[description = "Name of the level role stack"] name: String, - #[description = "Comma-separated list of role mentions (e.g. @Role1, @Role2, ...)"] - roles: String, -) -> Result<(), Error> { - let guild_id = ctx - .guild_id() - .ok_or_else(|| Error::msg("Guild only command"))?; - - let mut role_ids = Vec::new(); - - for part in roles.split(',') { - let part = part.trim(); - if let Some(id_str) = part.strip_prefix("<@&").and_then(|s| s.strip_suffix(">")) { - if let Ok(id) = id_str.parse::() { - role_ids.push(serenity::RoleId::new(id)); - } - } else if let Ok(id) = part.parse::() { - role_ids.push(serenity::RoleId::new(id)); - } - } - - if role_ids.is_empty() { - ctx.say("No valid roles found. Please use role mentions (e.g. @Role, @Role2, ...).") - .await?; - return Ok(()); - } - - let role_count = role_ids.len(); - - let db = &ctx.data().db; - - let role_ids_u64: Vec = role_ids.iter().map(|r| r.get()).collect(); - - tracing::info!("Updating level_role_stack for guild {}", guild_id); - - let updated: Option = db - .update(("guilds", guild_id.to_string())) - .merge(serde_json::json!({ - "level_role_stack": { - name.clone(): role_ids_u64 - } - })) - .await?; - - if updated.is_none() { - tracing::info!("Guild record not found, creating new record"); - // Record didn't exist, create it - let _: Option = db - .create(("guilds", guild_id.to_string())) - .content(serde_json::json!({ - "level_role_stack": { - name.clone(): role_ids_u64 - } - })) - .await?; - } - - ctx.say(format!( - "Level role stack `{}` updated successfully with {} roles.", - name, role_count - )) - .await?; - - Ok(()) -} - -#[poise::command(slash_command, prefix_command, guild_only)] -pub async fn get_level_roles(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx - .guild_id() - .ok_or_else(|| Error::msg("Guild only command"))?; - - let db = &ctx.data().db; - - let record: Option = db.select(("guilds", guild_id.to_string())).await?; - - let mut response = String::new(); - - if let Some(record) = record { - if let Some(stack) = record.level_role_stack { - if stack.is_empty() { - response = "No level role stacks found.".to_string(); - } else { - for (name, roles) in stack { - response.push_str(&format!("**{}:** ", name)); - let role_mentions: Vec = - roles.iter().map(|id| format!("<@&{}>", id)).collect(); - response.push_str(&role_mentions.join(", ")); - response.push('\n'); - } - } - } else { - response = "No level role stacks found.".to_string(); - } - } else { - response = "No configuration found for this guild.".to_string(); - } - - ctx.send( - poise::CreateReply::default() - .content(response) - .allowed_mentions(serenity::CreateAllowedMentions::new().empty_roles()), - ) - .await?; - - Ok(()) -} #[poise::command( slash_command, @@ -251,40 +213,7 @@ pub async fn set_levelup_message( Ok(()) } -#[poise::command( - slash_command, - prefix_command, - guild_only, - default_member_permissions = "MANAGE_GUILD", - required_permissions = "MANAGE_GUILD" -)] -pub async fn levelup_role_bridger( - ctx: Context<'_>, - #[description = "The role to bridge from"] in_role: serenity::Role, - #[description = "The role to bridge to"] out_role: serenity::Role, -) -> Result<(), Error> { - let guild_id = ctx - .guild_id() - .ok_or_else(|| Error::msg("Guild only command"))?; - let db = &ctx.data().db; - let _: Option = db - .update(("guilds", guild_id.to_string())) - .merge(serde_json::json!({ - "level_up_role_mapper": { - in_role.id.to_string(): out_role.id.get() - } - })) - .await?; - - ctx.say(format!( - "Role bridge created: {} -> {}", - in_role.name, out_role.name - )) - .await?; - - Ok(()) -} pub async fn process_message( ctx: &serenity::Context, @@ -360,67 +289,42 @@ pub async fn process_message( if let Some(record) = &guild_record { if let Some(stack) = &record.level_role_stack { - if let Some(roles) = stack.get(&level_data.track) { - // Check all roles in the stack for level requirements - // We need to fetch role names to check for patterns like "(min - max)" - // Try to get from cache first - let guild = guild_id.to_guild_cached(&ctx.cache).map(|g| g.clone()); - - // Regex to match "(min - max)" - let re = regex::Regex::new(r"\((\d+)\s*-\s*(\d+)\)").unwrap(); - - for role_id_u64 in roles { - let role_id = serenity::RoleId::new(*role_id_u64); - - // Try to find the role name - let role_name: Option = - if let Some(guild) = &guild { - guild.roles.get(&role_id).map(|r| r.name.clone()) - } else { - // Fallback to HTTP if not in cache (expensive but safe) - ctx.http.get_guild_roles(guild_id).await.ok().and_then( - |roles| { - roles - .iter() - .find(|r| r.id == role_id) - .map(|r| r.name.clone()) - }, + tracing::info!("Checking roles for track: {}", level_data.track); + if let Some(track_roles) = stack.get(&level_data.track) { + tracing::info!("Found {} roles in track {}", track_roles.len(), level_data.track); + // Find the role that matches the current level + for track_role in track_roles { + tracing::info!("Checking role {} for level {}", track_role.role_id, track_role.level); + if level_data.level == track_role.level { + let role_id = serenity::RoleId::new(track_role.role_id); + tracing::info!( + "Assigning role {} at level {} to user {}", + role_id, + track_role.level, + user_id + ); + if let Err(e) = ctx + .http + .add_member_role( + guild_id, + user_id, + role_id, + Some("Level up"), ) - }; - - if let Some(name) = role_name { - if let Some(caps) = re.captures(&name) { - if let (Ok(min), Ok(_max)) = - (caps[1].parse::(), caps[2].parse::()) - { - // Check if we just hit the min level - if level_data.level == min { - tracing::info!( - "Assigning role {} ({}) to user {}", - role_id, - name, - user_id - ); - if let Err(e) = ctx - .http - .add_member_role( - guild_id, - user_id, - role_id, - Some("Level up"), - ) - .await - { - tracing::error!("Failed to add role: {}", e); - } else { - role_assigned_id = Some(role_id); - } - } - } + .await + { + tracing::error!("Failed to add role: {}", e); + } else { + role_assigned_id = Some(role_id); } + break; // Only assign one role per level up } } + } else { + tracing::warn!("Track {} not found in guild {}", level_data.track, guild_id); } + } else { + tracing::warn!("No level role stack configured for guild {}", guild_id); } } @@ -502,11 +406,12 @@ pub async fn process_message( Err(_) => return Ok(()), }; - for (track_name, roles) in stack { - if let Some(first_role_id) = roles.first() { + for (track_name, track_roles) in stack { + // Find the starting role (the one with the lowest level, typically 0) + if let Some(starting_role) = track_roles.iter().min_by_key(|r| r.level) { if member .roles - .contains(&serenity::RoleId::new(*first_role_id)) + .contains(&serenity::RoleId::new(starting_role.role_id)) { // User has the starting role for this track let new_level = UserLevel { diff --git a/src/main.rs b/src/main.rs index 7d5296f..81e31c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -89,12 +89,9 @@ async fn main() -> Result<(), Error> { .options(FrameworkOptions:: { owners, commands: vec![ - commands::level::set_level_roles(), - commands::level::get_level_roles(), commands::level::leaderboard(), commands::level::set_levelup_message_channel(), commands::level::set_levelup_message(), - commands::level::levelup_role_bridger(), commands::fun::say(), commands::fun::urban(), commands::fun::ai_chat(), diff --git a/temp.txt b/temp.txt deleted file mode 100644 index 22c1c0e..0000000 --- a/temp.txt +++ /dev/null @@ -1,2 +0,0 @@ -the way you fixed it caused lot of problems -first of all it didn't show anything except level for avatar is showed nothing and for username it showed Unknwon User and urban command didn't work \ No newline at end of file