Overview
Faculty Bot features a sophisticated XP system that rewards user engagement through messages. The system uses logarithmic scaling to balance progression across different activity levels and provides visual feedback through level-up announcements.
How XP is Calculated
XP is awarded for every message sent in the server, with the amount calculated using a logarithmic scaling formula:
// Get message length
let content_len = new_message . content . chars () . count ();
// Base XP from message length
let base_xp = content_len as f64 / data . config . general . chars_for_level as f64 ;
// Logarithmic scaling: xp = base_xp / (1 + scale_factor * ln(level))
let scaling_factor = data . config . general . xp_scaling_factor;
let xp_to_add = base_xp / ( 1.0 + scaling_factor * ( user_data . user_level as f64 ) . ln ());
// Add to total
let new_xp = user_data . user_xp + xp_to_add ;
Reference: src/eventhandler.rs:86-92
Base XP Character count divided by configured threshold
Scaling Factor Reduces XP gain as level increases logarithmically
Natural Log Uses ln(level) to slow progression at higher levels
This formula ensures that:
New users level up quickly to stay engaged
Higher-level users still progress but at a slower rate
Message length matters, but isn’t exploitable
Level Calculation
Levels are calculated from total XP using a simple floor division:
// Level = floor(XP / 100)
let new_level = ( new_xp / 100.0 ) . floor () as i32 ;
Reference: src/eventhandler.rs:112
Every 100 XP equals one level.
Database Schema
User XP data is stored in PostgreSQL:
pub struct UserXP {
pub user_id : i64 , // Discord user ID
pub user_xp : f64 , // Total XP (decimal for precision)
pub user_level : i32 , // Current level
}
Reference: src/structs.rs:4-8
XP Tracking Flow
Message Event
User sends a message in any channel Bot messages and commands are ignored.
Fetch User Data
Query database for existing XP or create default entry let user_data = sqlx :: query_as :: < _ , structs :: UserXP >(
"SELECT * FROM user_xp WHERE user_id = $1"
)
. bind ( user_id )
. fetch_optional ( & mut pool )
. await ?
. unwrap_or_else ( || structs :: UserXP {
user_id ,
user_xp : 0.0 ,
user_level : 0 ,
});
Calculate XP
Apply scaling formula based on message length and current level
Update Database
Save new XP total using upsert query sqlx :: query (
"INSERT INTO user_xp (user_id, user_xp) VALUES ($1, $2)
ON CONFLICT (user_id) DO UPDATE SET user_xp = $2"
)
. bind ( user_id )
. bind ( new_xp )
. execute ( & mut pool )
. await ? ;
Check Level Up
If new level > old level, trigger level-up announcement
Reference: src/eventhandler.rs:55-143
Level-Up Announcements
When a user levels up, the bot:
Updates the level in the database
Generates a custom level-up image
Posts an announcement in the configured XP channel
if new_level > user_data . user_level {
// Update level
sqlx :: query (
"INSERT INTO user_xp (user_id, user_level) VALUES ($1, $2)
ON CONFLICT (user_id) DO UPDATE SET user_level = $2"
)
. bind ( user_id )
. bind ( new_level )
. execute ( & mut pool )
. await ? ;
// Generate level-up image
let img = utils :: show_levelup_image ( & new_message . author, new_level as u16 ) . await ? ;
// Post announcement
data . config . channels . xp
. send_message ( & ctx , | f | {
f . content ( format! (
"congrats {}! you've levelled up to {}!" ,
new_message . author . mention (),
new_level
))
. add_file ( AttachmentType :: Bytes {
data : std :: borrow :: Cow :: Borrowed ( & img ),
filename : "levelup.png" . to_string (),
})
})
. await ? ;
}
Reference: src/eventhandler.rs:113-141
Commands
/xp Display your current XP and level Shows personalized statistics for the user who runs the command.
/leaderboard View the top 10 users by XP Displays rank, username, and total XP for each user.
Leaderboard Implementation
The leaderboard queries the top 10 users ordered by XP:
let users = sqlx :: query_as :: < sqlx :: Postgres , structs :: UserXP >(
"SELECT * FROM user_xp ORDER BY user_xp DESC LIMIT 10" ,
)
. fetch_all ( pool )
. await ? ;
let mut leaderboard = String :: new ();
for ( i , user ) in users . iter () . enumerate () {
let user_discord = serenity :: UserId ( user . user_id as u64 )
. to_user ( & ctx . serenity_context ())
. await ? ;
leaderboard . push_str ( & format! (
"{}. {} - {} XP \n " ,
i + 1 ,
user_discord . tag () . replace ( "#0000" , "" ),
user . user_xp
));
}
Reference: src/commands/user.rs:216-236
Administrative Control
Manual XP Adjustment
Staff can manually set user XP using the /set-xp command:
pub async fn set_xp (
ctx : Context <' _ >,
user : serenity :: User ,
xp : i64 ,
) -> Result <(), Error > {
let uid = user . id . 0 as i64 ;
let new_level = ( xp as f64 / 100.0 ) . floor () as i32 ;
// Upsert user XP
sqlx :: query ( "INSERT INTO user_xp (user_id, user_xp, user_level) VALUES ($1, $2, $3)" )
. bind ( uid )
. bind ( xp as f64 )
. bind ( new_level )
. execute ( pool )
. await ? ;
}
Reference: src/commands/administration.rs:125-172
Requirements: Staff role or Administrator permission
Configuration Options
Number of characters needed for base XP calculation
Logarithmic scaling factor to reduce XP at higher levels
Channel where level-up announcements are posted
Key Features
Anti-Spam Protection Logarithmic scaling prevents spam from being rewarded excessively
Fair Progression New users level up quickly while veterans still progress meaningfully
Persistent Storage All XP data is stored in PostgreSQL for reliability
Visual Feedback Custom images celebrate level-up achievements