Skip to main content

Project Structure

Faculty Bot is organized into a modular structure for maintainability and clarity:
faculty_manager/
├── src/
│   ├── main.rs              # Application entry point
│   ├── commands/            # Discord command modules
│   │   ├── mod.rs           # Command registration
│   │   ├── user.rs          # User-facing commands
│   │   ├── administration.rs # Admin commands
│   │   └── moderation.rs    # Moderation commands
│   ├── web/                 # Web server (Rocket)
│   │   ├── mod.rs
│   │   ├── auth.rs          # Authentication/JWT
│   │   ├── api/             # API endpoints
│   │   └── structs.rs
│   ├── tasks/               # Background tasks
│   ├── eventhandler.rs      # Discord event handlers
│   ├── structs.rs           # Database models
│   ├── config.rs            # Configuration management
│   └── utils.rs             # Utility functions
├── i18n/                    # Internationalization
│   ├── en.json
│   ├── de.json
│   └── ja.json
├── migrations/              # Database migrations
│   └── faculty_manager.sql
├── templates/               # Web templates (Handlebars)
├── Cargo.toml               # Rust dependencies
├── config.json              # Bot configuration
└── .env                     # Environment variables

Core Components

1. Main Entry Point (main.rs)

The main.rs file orchestrates the entire application startup:
src/main.rs
#[tokio::main]
async fn main() {
    // Load environment variables
    dotenv().ok();
    
    // Start Rocket web server
    let rocket_result = rocket::build()
        .mount("/", routes![web::index, web::verify, ...])
        .mount("/api", routes![web::api::send_mail, ...])
        .attach(Template::fairing());
    
    // Run both bot and web server concurrently
    tokio::select! {
        _ = start_bot() => {},
        _ = rocket_result.launch() => {},
        _ = ctrl_z => { println!("Shutting down"); }
    }
}
Key responsibilities:
  • Initialize the Discord bot via start_bot()
  • Launch the Rocket web server for API endpoints
  • Set up graceful shutdown handling
  • Run both services concurrently with tokio::select!
See main.rs:106-134

2. Bot Initialization Flow

The start_bot() function sets up the Discord bot:
src/main.rs
async fn start_bot() -> Result<(), prelude::Error> {
    // 1. Load configuration
    let config: FacultyManagerConfig = config::read_config()?;
    
    // 2. Setup logging
    let tracing_layer = tracing_subscriber::EnvFilter::try_from_default_env()
        .or_else(|_| tracing_subscriber::EnvFilter::try_new("info"))?;
    
    // 3. Connect to PostgreSQL
    let pool = PgPoolOptions::new()
        .max_connections(15)
        .connect(&db_url)
        .await?;
    
    // 4. Initialize InfluxDB client for metrics
    let influx_client = influxdb2::Client::new(influx_host, influx_org, auth_token);
    
    // 5. Create email task channel
    let (tx, mut rx) = tokio::sync::mpsc::channel::<CurrentEmail>(100);
    
    // 6. Build Poise framework
    poise::Framework::builder()
        .options(poise::FrameworkOptions {
            commands: vec![
                register(),
                commands::user::verify(),
                commands::user::leaderboard(),
                // ...
            ],
            event_handler: |ctx, event, framework, data| {
                Box::pin(async move { 
                    eventhandler::event_listener(ctx, event, &framework, data).await 
                })
            },
            // ...
        })
        .token(token)
        .intents(GatewayIntents::all())
        .build()
        .await?
        .start_autosharded()
        .await?;
}
See main.rs:137-259

3. Data Struct (Global State)

The Data struct holds shared state accessible to all commands and handlers:
src/main.rs
#[derive(Clone)]
pub struct Data {
    pub db: sqlx::Pool<sqlx::Postgres>,              // Database connection pool
    pub config: config::FacultyManagerConfig,         // Bot configuration
    pub email_codes: DashMap<serenity::UserId, CodeEmailPair>, // Verification codes
    pub email_task: tokio::sync::mpsc::Sender<CurrentEmail>,   // Email queue
    pub influx: influxdb2::Client,                    // Metrics client
}
See main.rs:92-98 Usage in commands:
#[poise::command(slash_command)]
pub async fn my_command(ctx: Context<'_>) -> Result<(), Error> {
    let db = &ctx.data().db;           // Access database
    let config = &ctx.data().config;   // Access config
    // ...
}

4. Error Handling

Centralized error handling through a custom error enum:
src/main.rs
pub enum Error {
    Serenity(serenity::Error),      // Discord API errors
    Database(sqlx::Error),           // Database errors
    IO(std::io::Error),              // File/process errors
    NetRequest(reqwest::Error),      // HTTP request errors
    WithMessage(String),             // User-facing errors
    Migration(sqlx::migrate::MigrateError),
    Serde(serde_json::Error),
    ParseIntError(std::num::ParseIntError),
    Rss(rss::Error),
    Regex(regex::Error),
    Generic(GenericError),
    Unknown,
}
See main.rs:30-59

Command System

Command Organization

Commands are organized by category in src/commands/:
  • user.rs: Commands for all users (verify, xp, leaderboard)
  • administration.rs: Admin-only commands (getmail, set-xp, force-post-mensaplan)
  • moderation.rs: Moderation tools (pin, delete, promote/demote)

Command Registration

All commands are registered in main.rs:
src/main.rs
commands: vec![
    register(),
    commands::user::verify(),
    commands::user::leaderboard(),
    commands::user::xp(),
    commands::administration::getmail(),
    commands::administration::run_command(),
    commands::administration::set_xp(),
    commands::administration::force_post_mensaplan(),
    commands::administration::rule_command(),
    commands::administration::reverify(),
    commands::moderation::pin(),
    commands::moderation::delete_message(),
    commands::moderation::promote_user(),
    commands::moderation::demote_user(),
    commands::help(),
]
See main.rs:203-219

Event System

The event handler processes Discord events in eventhandler.rs:
src/eventhandler.rs
pub async fn event_listener(
    ctx: &serenity::Context,
    event: &poise::Event<'_>,
    fw: &poise::FrameworkContext<'_, Data, Error>,
    data: &Data,
) -> Result<(), Error> {
    match event {
        poise::Event::Ready { data_about_bot } => {
            // Bot ready - start background tasks
        },
        poise::Event::Message { new_message } => {
            // Process messages for XP
        },
        poise::Event::VoiceStateUpdate { old, new } => {
            // Handle voice channel creation/deletion
        },
        poise::Event::InteractionCreate { interaction } => {
            // Handle button interactions
        },
        _ => {}
    }
}
See eventhandler.rs:13-313

Background Tasks

Background tasks run concurrently with the bot in tasks.rs:

Task Types

  1. Mensaplan Posting (tasks.rs:33-117)
    • Checks for new meal plans at configured intervals
    • Posts to designated channel on specific weekdays
    • Prevents duplicate posts via database tracking
  2. RSS Feed Monitoring (tasks.rs:119-221)
    • Fetches RSS feeds at regular intervals
    • Posts new items or updates existing ones
    • Tracks posted items in database
  3. Latency Logging (tasks.rs:342-371)
    • Monitors bot latency to Discord API
    • Logs metrics to InfluxDB every 60 seconds

Web Server (Rocket)

The web component provides:
  • API Endpoints (src/web/api/): REST API for verification
  • Authentication (src/web/auth.rs): JWT-based authentication
  • Templates (templates/): Handlebars templates for web UI

Web Routes

src/main.rs
.mount("/", routes![
    web::index,
    web::verify,
    web::reverify,
    web::admin,
    web::login,
    web::logout,
    web::switch_account,
    web::setup
])
.mount("/api", routes![
    web::api::send_mail,
    web::api::check_code,
    web::api::discord_auth,
    web::api::discord_callback
])
See main.rs:112-119

Database Layer

The bot uses SQLx for compile-time checked SQL queries:
// Type-safe query with struct mapping
let user = sqlx::query_as::<sqlx::Postgres, structs::UserXP>(
    "SELECT * FROM user_xp WHERE user_id = $1"
)
.bind(user_id)
.fetch_optional(pool)
.await?;
All database models are defined in structs.rs.

Internationalization

The bot supports multiple languages using rosetta-i18n:
src/main.rs
pub mod translations {
    rosetta_i18n::include_translations!();
}
Translation files are compiled at build time from i18n/*.json files (see build.rs).

Concurrency Model

The application uses Tokio for async runtime:
  • Multi-threaded: Bot and web server run concurrently
  • Task spawning: Background tasks spawn independent Tokio tasks
  • Channel communication: MPSC channels for email queue
  • Shared state: Arc and DashMap for thread-safe state sharing

Configuration Management

Two-tiered configuration:
  1. Environment Variables (.env): Secrets and deployment-specific settings
  2. Config File (config.json): Bot behavior and Discord IDs
Configuration is loaded at startup and stored in the Data struct for global access.

Build docs developers (and LLMs) love