mod commands; mod data_loader; mod utils; mod racetime; use racetime::race::Races; use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::AtomicU64; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use serenity::async_trait; use serenity::prelude::*; use serenity::model::channel::{Message, Reaction}; use serenity::model::gateway::Ready; use serenity::model::id::GuildId; use serenity::http::Http; struct Owner; impl TypeMapKey for Owner { type Value = Arc; } struct Handler { is_loop_running: AtomicBool, } #[async_trait] impl EventHandler for Handler { // Set a handler for the `message` event - so that whenever a new message // is received - the closure (or function) passed will be called. // // Event handlers are dispatched through a threadpool, and so multiple // events can be dispatched simultaneously. async fn message(&self, ctx: Context, msg: Message) { commands::parse(ctx, msg).await; } async fn reaction_add(&self, ctx: Context, add_reaction: Reaction) { // println!("reaction_add event"); utils::Logger::log_message(ctx.clone(), &format!("reaction add event, emoji: {:#?}", add_reaction.emoji)).await; // todo!() commands::handle_reaction_add(ctx, add_reaction).await; } async fn reaction_remove(&self, ctx: Context, removed_reaction: Reaction) { // println!("reaction_remove event"); // utils::Logger::log_message(ctx, &format!("reaction removed event: {:#?}", removed_reaction)).await; commands::handle_reaction_remove(ctx, removed_reaction).await; } // Set a handler to be called on the `ready` event. This is called when a // shard is booted, and a READY payload is sent by Discord. This payload // contains data like the current user's guild Ids, current user data, // private channels, and more. // // In this case, just print what the current user's username is. async fn ready(&self, ctx: Context, ready: Ready) { println!("{} is connected!", ready.user.name); utils::Logger::log_message(ctx.clone(), "Start up and connection successful!").await; if let Err(why) = commands::initialize(ctx.clone()).await { utils::Logger::log_error(ctx.clone(), &format!("Failed to initialize commands module: {}", why)).await; } else { utils::Logger::log_message(ctx.clone(), "Module initialization successful!").await; } } // CODE TAKEN FROM EXAMPLE: // https://github.com/serenity-rs/serenity/blob/current/examples/e13_parallel_loops/src/main.rs // We use the cache_ready event just in case some cache operation is required in whatever use // case you have for this. async fn cache_ready(&self, ctx: Context, _guilds: Vec) { println!("Cache built successfully!"); utils::Logger::log_message(ctx.clone(), "Cache built successfully!").await; // it's safe to clone Context, but Arc is cheaper for this use case. // Untested claim, just theoretically. :P let ctx = Arc::new(ctx); // We need to check that the loop is not already running when this event triggers, // as this event triggers every time the bot enters or leaves a guild, along every time the // ready shard event triggers. // // An AtomicBool is used because it doesn't require a mutable reference to be changed, as // we don't have one due to self being an immutable reference. if !self.is_loop_running.load(Ordering::Relaxed) { // We have to clone the Arc, as it gets moved into the new thread. let ctx1 = Arc::clone(&ctx); // tokio::spawn creates a new green thread that can run in parallel with the rest of // the application. tokio::spawn(async move { loop { // We clone Context again here, because Arc is owned, so it moves to the // new function. //log_system_load(Arc::clone(&ctx1)).await; // TODO: function we want to run called here // Run racetime.gg and twitch api checks // Post new stuff to discord channel racetime::poll_races(Arc::clone(&ctx1)).await; tokio::time::sleep(Duration::from_secs(60)).await; } }); // Now that the loop is running, we set the bool to true self.is_loop_running.swap(true, Ordering::Relaxed); } } } #[tokio::main] async fn main() { let token = match data_loader::load_token("secrets/test.txt") { Ok(t) => t, Err(why) => panic!("Could not load app token: {}", why), }; // Fetch the application owner id let http = Http::new(&token); let owner = match http.get_current_application_info().await { Ok(info) => info.owner.id, Err(why) => panic!("Could not access application info: {:?}", why), }; //let race = Race { name: String::from("Test"), game: String::from("Test"), goal: String::from("Test"), status: Status::Open }; println!("Connecting..."); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGE_REACTIONS | GatewayIntents::MESSAGE_CONTENT; // Create a new instance of the Client, logging in as a bot. This will // automatically prepend your bot token with "Bot ", which is a requirement // by Discord for bot users. let mut client = Client::builder(&token, intents).event_handler(Handler { is_loop_running: AtomicBool::new(false), }).await.expect("Err creating client"); // Set the global data // https://github.com/serenity-rs/serenity/blob/current/examples/e12_global_data/src/main.rs { // Open the data lock in write mode, so keys can be inserted to it. let mut data = client.data.write().await; data.insert::(Arc::new(AtomicU64::new(owner.into()))); data.insert::(Arc::new(RwLock::new(HashMap::new()))); } // Finally, start a single shard, and start listening to events. // // Shards will automatically attempt to reconnect, and will perform // exponential backoff until it reconnects. if let Err(why) = client.start().await { println!("Client error: {:?}", why); } }