Fixed errors and performance issue
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user