Fixed errors and performance issue
This commit is contained in:
@@ -13,6 +13,7 @@ anyhow = "1.0.100"
|
|||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.42"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
futures = "0.3"
|
||||||
image = "0.25.9"
|
image = "0.25.9"
|
||||||
imageproc = "0.25.0"
|
imageproc = "0.25.0"
|
||||||
poise = "0.6.1"
|
poise = "0.6.1"
|
||||||
|
|||||||
@@ -166,37 +166,45 @@ pub async fn urban(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
while let Some(mci) = serenity::ComponentInteractionCollector::new(ctx)
|
loop {
|
||||||
.author_id(ctx.author().id)
|
let prev_id = prev_custom_id.clone();
|
||||||
.channel_id(ctx.channel_id())
|
let next_id = next_custom_id.clone();
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (embed, components) = create_page(current_page, &entries);
|
let mci = serenity::ComponentInteractionCollector::new(ctx)
|
||||||
|
.author_id(ctx.author().id)
|
||||||
if let Err(e) = mci
|
.channel_id(ctx.channel_id())
|
||||||
.create_response(
|
.timeout(std::time::Duration::from_secs(60 * 5))
|
||||||
ctx.http(),
|
.filter(move |mci| mci.data.custom_id == prev_id || mci.data.custom_id == next_id)
|
||||||
serenity::CreateInteractionResponse::UpdateMessage(
|
.await;
|
||||||
serenity::CreateInteractionResponseMessage::new()
|
|
||||||
.embed(embed)
|
if let Some(mci) = mci {
|
||||||
.components(components),
|
if mci.data.custom_id == prev_custom_id {
|
||||||
),
|
if current_page > 0 {
|
||||||
)
|
current_page -= 1;
|
||||||
.await
|
}
|
||||||
{
|
} else if mci.data.custom_id == next_custom_id {
|
||||||
tracing::error!("Failed to update urban message: {}", e);
|
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 serenity::prelude::TypeMapKey;
|
||||||
use surrealdb::Surreal;
|
use surrealdb::Surreal;
|
||||||
use surrealdb::engine::remote::ws::Client;
|
use surrealdb::engine::remote::ws::Client;
|
||||||
|
use futures::future::join_all;
|
||||||
|
|
||||||
pub struct DbKey;
|
pub struct DbKey;
|
||||||
|
|
||||||
@@ -509,6 +510,15 @@ pub async fn process_message(
|
|||||||
Ok(())
|
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)]
|
#[poise::command(slash_command, prefix_command, guild_only)]
|
||||||
pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let guild_id = ctx
|
let guild_id = ctx
|
||||||
@@ -520,10 +530,6 @@ pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
ctx.defer().await?;
|
ctx.defer().await?;
|
||||||
|
|
||||||
// Query top 10 users for this guild
|
// 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 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);
|
let prefix = format!("{}:", guild_id);
|
||||||
|
|
||||||
@@ -535,8 +541,80 @@ pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate image
|
// 1. Fetch all user data and avatars in parallel
|
||||||
let image_data = generate_leaderboard_image(&ctx, &entries).await?;
|
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(
|
ctx.send(
|
||||||
poise::CreateReply::default().attachment(serenity::CreateAttachment::bytes(
|
poise::CreateReply::default().attachment(serenity::CreateAttachment::bytes(
|
||||||
@@ -549,9 +627,8 @@ pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn generate_leaderboard_image(
|
fn generate_leaderboard_image(
|
||||||
ctx: &Context<'_>,
|
entries: Vec<LeaderboardRenderEntry>,
|
||||||
entries: &[LeaderboardEntry],
|
|
||||||
) -> Result<Vec<u8>, Error> {
|
) -> Result<Vec<u8>, Error> {
|
||||||
// Image dimensions
|
// Image dimensions
|
||||||
let width = 800;
|
let width = 800;
|
||||||
@@ -577,32 +654,6 @@ async fn generate_leaderboard_image(
|
|||||||
for (i, entry) in entries.iter().enumerate() {
|
for (i, entry) in entries.iter().enumerate() {
|
||||||
let y_offset = 100 + (i as u32 * 80);
|
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 Rank
|
||||||
draw_text_mut(
|
draw_text_mut(
|
||||||
&mut image,
|
&mut image,
|
||||||
@@ -611,20 +662,14 @@ async fn generate_leaderboard_image(
|
|||||||
y_offset as i32 + 20,
|
y_offset as i32 + 20,
|
||||||
scale_text,
|
scale_text,
|
||||||
&font,
|
&font,
|
||||||
&format!("#{}", i + 1),
|
&format!("#{}", entry.rank),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw Avatar
|
// Draw Avatar
|
||||||
let avatar_url = user.face();
|
if let Some(avatar_img) = &entry.avatar {
|
||||||
// We need to fetch the avatar image
|
let avatar_resized =
|
||||||
if let Ok(response) = reqwest::get(&avatar_url).await {
|
avatar_img.resize(60, 60, image::imageops::FilterType::Lanczos3);
|
||||||
if let Ok(bytes) = response.bytes().await {
|
image::imageops::overlay(&mut image, &avatar_resized, 80, y_offset as i64 + 10);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Username
|
// Draw Username
|
||||||
@@ -635,7 +680,7 @@ async fn generate_leaderboard_image(
|
|||||||
y_offset as i32 + 15,
|
y_offset as i32 + 15,
|
||||||
scale_text,
|
scale_text,
|
||||||
&font,
|
&font,
|
||||||
&user.name,
|
&entry.username,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw Level
|
// Draw Level
|
||||||
@@ -661,9 +706,8 @@ async fn generate_leaderboard_image(
|
|||||||
bar_bg,
|
bar_bg,
|
||||||
);
|
);
|
||||||
|
|
||||||
let next_level_xp = (entry.level + 1) * 100;
|
let progress = if entry.next_level_xp > 0 {
|
||||||
let progress = if next_level_xp > 0 {
|
entry.xp as f32 / entry.next_level_xp as f32
|
||||||
entry.xp as f32 / next_level_xp as f32
|
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
@@ -678,7 +722,7 @@ async fn generate_leaderboard_image(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw XP Text
|
// 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(
|
draw_text_mut(
|
||||||
&mut image,
|
&mut image,
|
||||||
white,
|
white,
|
||||||
|
|||||||
Reference in New Issue
Block a user