use std::{env, error::Error, path::PathBuf, sync::Arc}; use brain::Brain; use confindent::Confindent; use database::Database; use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType}; use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt}; use twilight_http::Client as HttpClient; use twilight_model::{application::interaction::InteractionData, id::Id}; use crate::command::Commands; mod brain; mod command; mod database; mod import; mod util; const PROD_APP_ID: u64 = 1363055126264283136; #[allow(dead_code)] const DEV_APP_ID: u64 = 1363494986699771984; const APP_ID: u64 = PROD_APP_ID; #[tokio::main] async fn main() -> anyhow::Result<()> { //dotenv::dotenv().ok(); let conf_path = if !PathBuf::from("/etc/leaberblord.conf").exists() { "leaberblord.conf" } else { "/etc/leaberblord.conf" }; let conf = Confindent::from_file(conf_path).unwrap(); let db_dir = conf .child_owned("Database") .unwrap_or("/var/leaberblord/leaberblord.sqlite".to_string()); let db = Database::new(db_dir); db.create_tables(); // Look for an `import` CLI-thing so we know if we should impor and emboard // database from local let arg = std::env::args().nth(1); match arg.as_deref() { Some("import") => { let Some(file) = std::env::args().nth(2) else { eprintln!("import subcommand requires a file path argument to read data from."); return Ok(()); }; let data = std::fs::read_to_string(file).unwrap(); import::import(&db, data); println!("Import finished!"); return Ok(()); } _ => (), } // Finally, setup and start the bot itself let TwilightStuff { mut shard, http, cache, } = setup_twilight(&conf).await; let brain = Arc::new(Brain::new(db, http, cache)); while let Some(item) = shard.next_event(EventTypeFlags::all()).await { let Ok(event) = item else { eprintln!("error receiving event"); continue; }; brain.update_cache(&event); tokio::spawn(handle_event(event, Arc::clone(&brain))); } Ok(()) } /// A more proper container than a tuple for everything we get from [setup_twilight] struct TwilightStuff { shard: Shard, http: HttpClient, cache: DefaultInMemoryCache, } /// Configure enough to get off the ground with twilight. /// - Get the `DISCORD_TOKEN` from the environment or `DiscordToken` from the conf file, with that preference /// - Setup ourself as Shard 1 and take the `GUILD_MESSAGES` intent /// - Register with the API and create our `InMemoryCache` /// - Register all of our commands async fn setup_twilight(conf: &Confindent) -> TwilightStuff { let token = env::var("DISCORD_TOKEN") .ok() .unwrap_or_else(|| conf.child_owned("DiscordToken").unwrap()); let shard = Shard::new(ShardId::ONE, token.clone(), Intents::GUILD_MESSAGES); let http = HttpClient::new(token); let cache = DefaultInMemoryCache::builder() .resource_types(ResourceType::GUILD) .resource_types(ResourceType::MESSAGE) .resource_types(ResourceType::MEMBER) .build(); let iclient = http.interaction(Id::new(APP_ID)); let commands_vec = command::build().await; iclient.set_global_commands(&commands_vec).await.unwrap(); println!("Set commands!"); let cmd_ap_global = iclient .global_commands() .await .unwrap() .models() .await .unwrap(); for cmd in cmd_ap_global { println!( "global command- [{}] {}: {}", cmd.id .map(|c| c.get().to_string()) .unwrap_or("-".to_owned()), cmd.name, cmd.description ); } TwilightStuff { shard, http, cache } } /// Handle all of our events. This is our main work function and what main /// tends to branch into, but we don't loop in the function itself. async fn handle_event(event: Event, brain: Arc) -> Result<(), Box> { match event { Event::InteractionCreate(create) => { // Don't allow direct message commanding if create.is_dm() { brain .interaction_respond(&create, fail!("leaderboards not supported in DMs")) .await; return Ok(()); } let data = create.data.as_ref().unwrap(); if let InteractionData::ApplicationCommand(cmd) = data { let guild = create.guild.as_ref().unwrap().id.unwrap(); let command = match cmd.name.as_str() { "about" => Commands::About, "leaderboard" => Commands::Leaderboard, "points" => Commands::Points, "permission" => Commands::Permission, "import" => Commands::Import, _ => panic!("'{}' is not a command", cmd.name), }; command::handler(brain, guild, &create, command, cmd).await; } } Event::GatewayClose(close) => { println!("GatewayClose - {close:?}") } _ => (), } Ok(()) }