Added endpoints for level and leaderboard

This commit is contained in:
2026-01-02 01:58:02 +05:30
parent d7e28b6d91
commit cb12b8ef75
4 changed files with 591 additions and 241 deletions

View File

@@ -38,17 +38,94 @@ pub struct LeaderboardMember {
pub struct Role { pub struct Role {
pub role_id: String, pub role_id: String,
pub role_name: String, pub role_name: String,
pub color: u32,
pub position: u16,
} }
#[derive(Serialize)] #[derive(Serialize, Clone, Debug)]
pub struct LevelRole { pub struct TrackLevelRole {
pub id: String, pub role_id: u64,
pub name: 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,
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)] #[derive(Deserialize)]
struct GuildRecord { 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)] #[derive(Deserialize, Debug)]
@@ -275,6 +352,8 @@ async fn get_roles(
.map(|(role_id, role)| Role { .map(|(role_id, role)| Role {
role_id: role_id.to_string(), role_id: role_id.to_string(),
role_name: role.name.clone(), role_name: role.name.clone(),
color: role.colour.0,
position: role.position,
}) })
.collect(); .collect();
@@ -288,14 +367,13 @@ async fn get_level_tracks(
data: web::Data<ApiState>, data: web::Data<ApiState>,
) -> Result<HttpResponse, actix_web::Error> { ) -> Result<HttpResponse, actix_web::Error> {
let guild_id_value = guild_id.into_inner(); 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 // Check if bot is in the guild
let guild = data.cache.guild(guild_id_value) if data.cache.guild(guild_id_value).is_none() {
.ok_or_else(|| { warn!("Bot is not in guild {}", guild_id_value);
warn!("Bot is not in guild {}", guild_id_value); return Err(error::ErrorNotFound("Bot is not in this guild"));
error::ErrorNotFound("Bot is not in this guild") }
})?;
// Query the guild record from database // Query the guild record from database
let record: Option<GuildRecord> = data.db.select(("guilds", guild_id_value.to_string())).await 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") 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(record) = record {
if let Some(level_role_stack) = record.level_role_stack { if let Some(level_role_stack) = record.level_role_stack {
for (track_name, role_ids) in level_role_stack { for (track_name, mut track_roles) in level_role_stack {
let mut roles: Vec<LevelRole> = Vec::new(); // Sort by level ascending
for role_id in role_ids { track_roles.sort_by_key(|r| r.level);
let role_id_obj = serenity::all::RoleId::new(role_id); result.insert(track_name, track_roles);
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);
} }
} }
} }
@@ -330,6 +398,381 @@ async fn get_level_tracks(
Ok(HttpResponse::Ok().json(result)) 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( pub async fn start_api_server(
cache: Arc<Cache>, cache: Arc<Cache>,
db: Surreal<Client>, db: Surreal<Client>,
@@ -348,6 +791,13 @@ pub async fn start_api_server(
.service(get_leaderboard) .service(get_leaderboard)
.service(get_roles) .service(get_roles)
.service(get_level_tracks) .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))? .bind(("0.0.0.0", port))?
.run() .run()

View File

@@ -7,7 +7,6 @@ use poise::serenity_prelude as serenity;
use serde::Deserialize; use serde::Deserialize;
use serenity::Mentionable; use serenity::Mentionable;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Cursor;
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use std::time::Duration; use std::time::Duration;
@@ -52,9 +51,88 @@ pub struct UserLevel {
pub last_message: chrono::DateTime<chrono::Utc>, pub last_message: chrono::DateTime<chrono::Utc>,
} }
/// 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<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)] #[derive(Deserialize)]
struct GuildRecord { struct GuildRecord {
level_role_stack: Option<HashMap<String, Vec<u64>>>, level_role_stack: Option<HashMap<String, Vec<TrackLevelRole>>>,
levelup_channel: Option<u64>, levelup_channel: Option<u64>,
levelup_message: Option<String>, levelup_message: Option<String>,
level_up_role_mapper: Option<HashMap<String, u64>>, level_up_role_mapper: Option<HashMap<String, u64>>,
@@ -67,122 +145,6 @@ struct LeaderboardEntry {
level: u64, 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::<u64>() {
role_ids.push(serenity::RoleId::new(id));
}
} else if let Ok(id) = part.parse::<u64>() {
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<u64> = role_ids.iter().map(|r| r.get()).collect();
tracing::info!("Updating level_role_stack for guild {}", guild_id);
let updated: Option<serde::de::IgnoredAny> = 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<serde::de::IgnoredAny> = 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<GuildRecord> = 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<String> =
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( #[poise::command(
slash_command, slash_command,
@@ -251,40 +213,7 @@ pub async fn set_levelup_message(
Ok(()) 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<serde::de::IgnoredAny> = 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( pub async fn process_message(
ctx: &serenity::Context, ctx: &serenity::Context,
@@ -360,67 +289,42 @@ pub async fn process_message(
if let Some(record) = &guild_record { if let Some(record) = &guild_record {
if let Some(stack) = &record.level_role_stack { if let Some(stack) = &record.level_role_stack {
if let Some(roles) = stack.get(&level_data.track) { tracing::info!("Checking roles for track: {}", level_data.track);
// Check all roles in the stack for level requirements if let Some(track_roles) = stack.get(&level_data.track) {
// We need to fetch role names to check for patterns like "(min - max)" tracing::info!("Found {} roles in track {}", track_roles.len(), level_data.track);
// Try to get from cache first // Find the role that matches the current level
let guild = guild_id.to_guild_cached(&ctx.cache).map(|g| g.clone()); for track_role in track_roles {
tracing::info!("Checking role {} for level {}", track_role.role_id, track_role.level);
// Regex to match "(min - max)" if level_data.level == track_role.level {
let re = regex::Regex::new(r"\((\d+)\s*-\s*(\d+)\)").unwrap(); let role_id = serenity::RoleId::new(track_role.role_id);
tracing::info!(
for role_id_u64 in roles { "Assigning role {} at level {} to user {}",
let role_id = serenity::RoleId::new(*role_id_u64); role_id,
track_role.level,
// Try to find the role name user_id
let role_name: Option<String> = );
if let Some(guild) = &guild { if let Err(e) = ctx
guild.roles.get(&role_id).map(|r| r.name.clone()) .http
} else { .add_member_role(
// Fallback to HTTP if not in cache (expensive but safe) guild_id,
ctx.http.get_guild_roles(guild_id).await.ok().and_then( user_id,
|roles| { role_id,
roles Some("Level up"),
.iter()
.find(|r| r.id == role_id)
.map(|r| r.name.clone())
},
) )
}; .await
{
if let Some(name) = role_name { tracing::error!("Failed to add role: {}", e);
if let Some(caps) = re.captures(&name) { } else {
if let (Ok(min), Ok(_max)) = role_assigned_id = Some(role_id);
(caps[1].parse::<u64>(), caps[2].parse::<u64>())
{
// 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);
}
}
}
} }
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(()), Err(_) => return Ok(()),
}; };
for (track_name, roles) in stack { for (track_name, track_roles) in stack {
if let Some(first_role_id) = roles.first() { // 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 if member
.roles .roles
.contains(&serenity::RoleId::new(*first_role_id)) .contains(&serenity::RoleId::new(starting_role.role_id))
{ {
// User has the starting role for this track // User has the starting role for this track
let new_level = UserLevel { let new_level = UserLevel {

View File

@@ -89,12 +89,9 @@ async fn main() -> Result<(), Error> {
.options(FrameworkOptions::<Data, Error> { .options(FrameworkOptions::<Data, Error> {
owners, owners,
commands: vec![ commands: vec![
commands::level::set_level_roles(),
commands::level::get_level_roles(),
commands::level::leaderboard(), commands::level::leaderboard(),
commands::level::set_levelup_message_channel(), commands::level::set_levelup_message_channel(),
commands::level::set_levelup_message(), commands::level::set_levelup_message(),
commands::level::levelup_role_bridger(),
commands::fun::say(), commands::fun::say(),
commands::fun::urban(), commands::fun::urban(),
commands::fun::ai_chat(), commands::fun::ai_chat(),

View File

@@ -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