From d216879a71803e53ce92ed4e4590b240c5baf481 Mon Sep 17 00:00:00 2001 From: Kishor Date: Mon, 29 Dec 2025 12:32:11 +0530 Subject: [PATCH] Fixed errors and performance issue --- Cargo.toml | 1 + src/commands/fun.rs | 68 +++++++++++--------- src/commands/level.rs | 146 +++++++++++++++++++++++++++--------------- 3 files changed, 134 insertions(+), 81 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e389cc1..89c296e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ anyhow = "1.0.100" base64 = "0.22.1" chrono = "0.4.42" dotenvy = "0.15.7" +futures = "0.3" image = "0.25.9" imageproc = "0.25.0" poise = "0.6.1" diff --git a/src/commands/fun.rs b/src/commands/fun.rs index 49382d1..386878b 100644 --- a/src/commands/fun.rs +++ b/src/commands/fun.rs @@ -166,37 +166,45 @@ pub async fn urban( ) .await?; - while let Some(mci) = serenity::ComponentInteractionCollector::new(ctx) - .author_id(ctx.author().id) - .channel_id(ctx.channel_id()) - .timeout(std::time::Duration::from_secs(60 * 5)) - .filter(move |mci| mci.data.custom_id == prev_custom_id || mci.data.custom_id == next_custom_id) - .await - { - if mci.data.custom_id == prev_custom_id { - if current_page > 0 { - current_page -= 1; - } - } else if mci.data.custom_id == next_custom_id { - if current_page < entries.len() - 1 { - current_page += 1; - } - } + loop { + let prev_id = prev_custom_id.clone(); + let next_id = next_custom_id.clone(); - let (embed, components) = create_page(current_page, &entries); - - if let Err(e) = mci - .create_response( - ctx.http(), - serenity::CreateInteractionResponse::UpdateMessage( - serenity::CreateInteractionResponseMessage::new() - .embed(embed) - .components(components), - ), - ) - .await - { - tracing::error!("Failed to update urban message: {}", e); + let mci = serenity::ComponentInteractionCollector::new(ctx) + .author_id(ctx.author().id) + .channel_id(ctx.channel_id()) + .timeout(std::time::Duration::from_secs(60 * 5)) + .filter(move |mci| mci.data.custom_id == prev_id || mci.data.custom_id == next_id) + .await; + + if let Some(mci) = mci { + if mci.data.custom_id == prev_custom_id { + if current_page > 0 { + current_page -= 1; + } + } else if mci.data.custom_id == next_custom_id { + if current_page < entries.len() - 1 { + current_page += 1; + } + } + + let (embed, components) = create_page(current_page, &entries); + + if let Err(e) = mci + .create_response( + ctx.http(), + serenity::CreateInteractionResponse::UpdateMessage( + serenity::CreateInteractionResponseMessage::new() + .embed(embed) + .components(components), + ), + ) + .await + { + tracing::error!("Failed to update urban message: {}", e); + } + } else { + break; } } diff --git a/src/commands/level.rs b/src/commands/level.rs index 662df9f..2917247 100644 --- a/src/commands/level.rs +++ b/src/commands/level.rs @@ -12,6 +12,7 @@ use std::io::Cursor; use serenity::prelude::TypeMapKey; use surrealdb::Surreal; use surrealdb::engine::remote::ws::Client; +use futures::future::join_all; pub struct DbKey; @@ -509,6 +510,15 @@ pub async fn process_message( Ok(()) } +struct LeaderboardRenderEntry { + username: String, + rank: usize, + level: u64, + xp: u64, + next_level_xp: u64, + avatar: Option, +} + #[poise::command(slash_command, prefix_command, guild_only)] pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx @@ -520,10 +530,6 @@ pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> { ctx.defer().await?; // Query top 10 users for this guild - // We filter by ID starting with "levels:guild_id:" - // Note: In SurrealDB, record IDs are `table:id_part`. - // The id_part here is `guild_id:user_id`. - // We can use string functions on the ID. let sql = "SELECT * FROM levels WHERE string::starts_with(record::id(id), $prefix) ORDER BY level DESC, xp DESC LIMIT 10"; let prefix = format!("{}:", guild_id); @@ -535,8 +541,80 @@ pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> { return Ok(()); } - // Generate image - let image_data = generate_leaderboard_image(&ctx, &entries).await?; + // 1. Fetch all user data and avatars in parallel + let mut tasks = Vec::new(); + + for (i, entry) in entries.iter().enumerate() { + let ctx = ctx.clone(); // Clone for the move into async block + let entry_level = entry.level; + let entry_xp = entry.xp; + + // Parse user ID + let id_str = entry.id.id.to_string(); + let clean_id_str = id_str.trim_matches(|c| c == '"' || c == '<' || c == '>').to_string(); + + tasks.push(async move { + let parts: Vec<&str> = clean_id_str.split(':').collect(); + let user_id_str = parts.last().unwrap_or(&"0"); + let user_id_u64 = user_id_str.parse::().unwrap_or(0); + + let user_name; + let avatar_url; + + if user_id_u64 != 0 { + let user_id = serenity::UserId::new(user_id_u64); + // Try cache first + if let Some(user) = user_id.to_user_cached(&ctx) { + user_name = user.name.clone(); + avatar_url = user.face(); + } else { + // Fallback to HTTP + match ctx.http().get_user(user_id).await { + Ok(user) => { + user_name = user.name; + avatar_url = user.face(); + } + Err(_) => { + user_name = "Unknown User".to_string(); + avatar_url = String::new(); // Or default avatar URL + } + } + } + } else { + user_name = "Unknown User".to_string(); + avatar_url = String::new(); + } + + // Fetch avatar image if we have a URL + let mut avatar_img = None; + if !avatar_url.is_empty() { + if let Ok(response) = reqwest::get(&avatar_url).await { + if let Ok(bytes) = response.bytes().await { + if let Ok(img) = image::load_from_memory(&bytes) { + avatar_img = Some(img); + } + } + } + } + + LeaderboardRenderEntry { + username: user_name, + rank: i + 1, + level: entry_level, + xp: entry_xp, + next_level_xp: (entry_level + 1) * 100, + avatar: avatar_img, + } + }); + } + + // Wait for all fetches to complete + let render_entries = join_all(tasks).await; + + // 2. Generate image on a blocking thread + let image_data = tokio::task::spawn_blocking(move || { + generate_leaderboard_image(render_entries) + }).await??; ctx.send( poise::CreateReply::default().attachment(serenity::CreateAttachment::bytes( @@ -549,9 +627,8 @@ pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> { Ok(()) } -async fn generate_leaderboard_image( - ctx: &Context<'_>, - entries: &[LeaderboardEntry], +fn generate_leaderboard_image( + entries: Vec, ) -> Result, Error> { // Image dimensions let width = 800; @@ -577,32 +654,6 @@ async fn generate_leaderboard_image( for (i, entry) in entries.iter().enumerate() { let y_offset = 100 + (i as u32 * 80); - // Extract user ID from record ID - // id.id is String("guild_id:user_id") - let id_str = entry.id.id.to_string(); - let clean_id_str = id_str.trim_matches(|c| c == '"' || c == '⟨' || c == '⟩'); - let parts: Vec<&str> = clean_id_str.split(':').collect(); - let user_id_str = parts.last().unwrap_or(&"0"); - let user_id = user_id_str.parse::().unwrap_or(0); - - // Fetch user info - let user = if user_id != 0 { - match ctx.http().get_user(serenity::UserId::new(user_id)).await { - Ok(u) => u, - Err(_) => { - // Fallback if user not found - let mut u = serenity::User::default(); - u.name = "Unknown User".to_string(); - u - } - } - } else { - tracing::warn!("Invalid user ID parsed from {}: {}", id_str, user_id_str); - let mut u = serenity::User::default(); - u.name = "Unknown User".to_string(); - u - }; - // Draw Rank draw_text_mut( &mut image, @@ -611,20 +662,14 @@ async fn generate_leaderboard_image( y_offset as i32 + 20, scale_text, &font, - &format!("#{}", i + 1), + &format!("#{}", entry.rank), ); // Draw Avatar - let avatar_url = user.face(); - // We need to fetch the avatar image - if let Ok(response) = reqwest::get(&avatar_url).await { - if let Ok(bytes) = response.bytes().await { - if let Ok(avatar_img) = image::load_from_memory(&bytes) { - let avatar_resized = - avatar_img.resize(60, 60, image::imageops::FilterType::Lanczos3); - image::imageops::overlay(&mut image, &avatar_resized, 80, y_offset as i64 + 10); - } - } + if let Some(avatar_img) = &entry.avatar { + let avatar_resized = + avatar_img.resize(60, 60, image::imageops::FilterType::Lanczos3); + image::imageops::overlay(&mut image, &avatar_resized, 80, y_offset as i64 + 10); } // Draw Username @@ -635,7 +680,7 @@ async fn generate_leaderboard_image( y_offset as i32 + 15, scale_text, &font, - &user.name, + &entry.username, ); // Draw Level @@ -661,9 +706,8 @@ async fn generate_leaderboard_image( bar_bg, ); - let next_level_xp = (entry.level + 1) * 100; - let progress = if next_level_xp > 0 { - entry.xp as f32 / next_level_xp as f32 + let progress = if entry.next_level_xp > 0 { + entry.xp as f32 / entry.next_level_xp as f32 } else { 0.0 }; @@ -678,7 +722,7 @@ async fn generate_leaderboard_image( } // Draw XP Text - let xp_text = format!("{}/{} XP", entry.xp, next_level_xp); + let xp_text = format!("{}/{} XP", entry.xp, entry.next_level_xp); draw_text_mut( &mut image, white,