mod commands; mod events; mod handlers; mod structs; mod utils; use std::sync::Arc; use events::event_handler; use handlers::db::DatabaseController; use serde::{Deserialize, Serialize}; use serenity::{ all::{ClientBuilder, GatewayIntents}, futures::lock::Mutex, }; use sqlx::mysql::MySqlPoolOptions; use sqlx::MySqlPool; use structs::vouch::Vouch; use tracing::{event, info, info_span, Level}; fn check_required_env_vars() { let env_span = info_span!("check_required_env_vars"); let required_vars = vec!["DATABASE_URL", "BOT_TOKEN"]; // Enter into the span then check the required environment variables let _enter = env_span.enter(); for var in required_vars { info!("checking {}", var); if std::env::var(var).is_err() { event!( Level::ERROR, "required environment variable {} is not set", var ); panic!( "required environment variable {} is not set, cannot continue", var ); } } info!("all required environment variables are set"); // Exit the span drop(_enter); } static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations"); async fn init_sqlx() -> MySqlPool { let sqlx_span = info_span!("init_sqlx"); // Enter into the span then initialize SQLx let _enter = sqlx_span.enter(); // Create a pooled connection to the database info!("creating a database connection pool"); let pool = MySqlPoolOptions::new() .max_connections(5) .connect(&std::env::var("DATABASE_URL").unwrap()) .await .expect("failed to create a database connection pool"); info!("database connection pool created"); // Ensure database schema is up to date info!("running database migrations"); MIGRATOR .run(&pool) .await .expect("Migrations did not succeed"); info!("database migrations completed"); info!("SQLx initialized"); // Exit the span drop(_enter); pool } struct Data { database_controller: DatabaseController, owners: Vec<u64>, uptime: std::time::Instant, config: Config, vouch_store: Mutex<Vec<Vouch>>, } // User data, which is stored and accessible in all command invocations pub type Error = Box<dyn std::error::Error + Send + Sync + 'static>; type Context<'a> = poise::Context<'a, Data, Error>; #[derive(Deserialize, Serialize)] struct Config { main_guild_id: u64, channels: Channels, roles: Roles, } #[derive(Deserialize, Serialize)] struct Channels { welcome: u64, main: u64, logs_public: u64, logs_mod: u64, starboard: u64, } #[derive(Deserialize, Serialize)] struct Roles { admin: u64, silly_role: u64, } #[tokio::main] async fn main() { let start_time = std::time::Instant::now(); tracing_subscriber::fmt::init(); let init_span = info_span!("init"); let main_span = info_span!("main"); let _enter = init_span.enter(); info!("loading dotenv"); dotenv::dotenv().ok(); info!("checking required environment variables"); check_required_env_vars(); info!("loading config"); // Do we have a config.toml file? If we do, load it // If we don't, create it with the default values, then exit the program and tell the user to fill it out let config: Config = match std::fs::read_to_string("config.toml") { Ok(config) => toml::from_str(&config).expect("failed to parse config.toml"), Err(_) => { let default_config = Config { main_guild_id: 0, channels: Channels { welcome: 0, main: 0, logs_public: 0, logs_mod: 0, starboard: 0, }, roles: Roles { admin: 0, silly_role: 0, }, }; let default_config_toml = toml::to_string_pretty(&default_config).unwrap(); std::fs::write("config.toml", default_config_toml) .expect("failed to write config.toml"); event!(Level::WARN, "config.toml not found, created a default one"); event!( Level::ERROR, "please fill out config.toml and restart the bot" ); panic!("config.toml not found, created a default one, please fill it out and restart the bot"); } }; info!("initializing SQLx"); let pool = init_sqlx().await; info!("initializing bot"); let intents = GatewayIntents::privileged() | GatewayIntents::GUILD_MEMBERS | GatewayIntents::GUILD_MESSAGES; let framework = poise::Framework::builder() .options(poise::FrameworkOptions::<Data, Error> { commands: vec![ commands::ping::ping(), commands::vouch::vouch(), commands::profile::profiles(), commands::dog::dog(), commands::cta::cta(), commands::action::use_action_hug(), commands::action::use_action_kiss(), commands::action::use_action_pat(), commands::eval::eval(), commands::quote::quote_action(), commands::quote::random_quote(), commands::quote::user_quotes(), ], event_handler: |ctx, event, framework, data| { Box::pin(event_handler(ctx, event, framework, data)) }, prefix_options: poise::PrefixFrameworkOptions { prefix: Some("~".into()), edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan( std::time::Duration::from_secs(3600), ))), case_insensitive_commands: true, ..Default::default() }, ..Default::default() }) .setup(|ctx, _ready, framework| { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; Ok(Data { // Initialize user data here database_controller: DatabaseController::new(pool.clone()), uptime: std::time::Instant::now(), config, vouch_store: Mutex::new(Vec::new()), // Sticks, Emi, Katie owners: vec![1017196087276220447, 272871217256726531, 1033331958291369984], }) }) }) .build(); let mut client = ClientBuilder::new(std::env::var("BOT_TOKEN").unwrap(), intents) .framework(framework) .await .expect("Error creating client"); info!("bot initialized"); drop(_enter); let _enter = main_span.enter(); info!("init done in {:?}", start_time.elapsed()); info!("starting bot"); if let Err(why) = client.start().await { event!(Level::ERROR, "Client error: {:?}", why); } }