Fixed errors and performance issue

This commit is contained in:
2025-12-29 12:32:11 +05:30
parent 7468216754
commit d216879a71
3 changed files with 134 additions and 81 deletions

View File

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

View File

@@ -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);
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 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);
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;
}
}

View File

@@ -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<image::DynamicImage>,
}
#[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::<u64>().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<LeaderboardRenderEntry>,
) -> Result<Vec<u8>, 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::<u64>().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,