Skip to main content

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

Scaling Formula Breakdown

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

1

Message Event

User sends a message in any channelBot messages and commands are ignored.
2

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,
});
3

Calculate XP

Apply scaling formula based on message length and current level
4

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?;
5

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:
  1. Updates the level in the database
  2. Generates a custom level-up image
  3. 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 levelShows personalized statistics for the user who runs the command.

/leaderboard

View the top 10 users by XPDisplays 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

chars_for_level
integer
Number of characters needed for base XP calculation
xp_scaling_factor
float
Logarithmic scaling factor to reduce XP at higher levels
channels.xp
ChannelId
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

Build docs developers (and LLMs) love