use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Condvar, OnceLock}; use crate::commands::{self, COMMANDS}; use crate::{constants, tasks, CARG_NO_DISCORD}; use serenity::all::{ActivityData, Context, GuildId}; use serenity::all::{EventHandler, GatewayIntents, ShardManager}; use serenity::model::channel::Message; use serenity::model::gateway::Ready; use serenity::Client; use tracing::{error, info, warn}; pub static DO_SHUTDOWN: (AtomicBool, Condvar) = (AtomicBool::new(false), Condvar::new()); // atombool and condvar combo to ensure maximum coverage when the bot needs to power off pub static SHARD_MANAGER: OnceLock> = OnceLock::new(); pub struct BotHandler; unsafe impl Sync for BotHandler {} unsafe impl Send for BotHandler {} #[serenity::async_trait] impl EventHandler for BotHandler { async fn message(&self, ctx: Context, msg: Message) { // We do not reply to bots https://en.wikipedia.org/wiki/Email_storm if msg.author.bot { return; } let prefix_regex = format!( r"^({}|{}|<@{}>)\s?", constants::COMMAND_PREFIX, "NPO_PFX", ctx.cache.current_user().id ); let cmd_regex = format!(r"{}[A-Za-z0-9_\-]+", prefix_regex); let result = match regex::Regex::new(cmd_regex.as_str()) .unwrap() .find(&msg.content) { Some(result) => result, None => return, // silently exit because not every message is meant for the bot }; if DO_SHUTDOWN.0.load(Ordering::SeqCst) { let _ = msg .channel_id .say( &ctx.http, "Sorry! Your request was cancelled because the bot is shutting down.", ) .await; return; } let target_cmd_name = regex::Regex::new(prefix_regex.as_str()) .unwrap() .replace(result.as_str(), "") .to_string(); msg.reply(&ctx.http, target_cmd_name.to_string()) .await .unwrap(); if let Some(command) = COMMANDS.read().await.get(&target_cmd_name) { if let Some(guild_id) = msg.guild_id { if let Some(run_guild_command) = &command.run_guild_command { match run_guild_command { commands::CommandFnKind::Lua(_) => todo!(), commands::CommandFnKind::Rust(cmd) => (cmd)(ctx, msg, Some(guild_id)), } } } else { if let Some(run_dm_command) = &command.run_dm_command { match run_dm_command { commands::CommandFnKind::Lua(_) => todo!(), commands::CommandFnKind::Rust(cmd) => (cmd)(ctx, msg, None), } } } } } /// Runs once for every shard once its ready async fn ready(&self, ctx: Context, ready: Ready) { info!("Shart `{}` is connected!", ready.shard.unwrap().id); ctx.set_activity(Some(ActivityData::custom("Initializing."))) } /// Runs once when all shards are ready async fn shards_ready(&self, ctx: Context, total_shards: u32) { tasks::start_tasks(ctx).await; info!("{total_shards} shards ready."); } // Runs once for every shard when its cache is ready async fn cache_ready(&self, _ctx: Context, _guild_id_list: Vec) { info!("Cache ready."); } } pub async fn start() { // TODO load this at runtime so the key will not be stored in the binary? #[cfg(not(debug_assertions))] let token = { info!("Initializing bot with production token."); include_str!("bot_token.prod") }; #[cfg(debug_assertions)] let token = { info!("Initializing bot with development token."); include_str!("bot_token.dev") }; let intents = GatewayIntents::DIRECT_MESSAGES | GatewayIntents::DIRECT_MESSAGE_REACTIONS | GatewayIntents::GUILDS | GatewayIntents::GUILD_MODERATION // | GatewayIntents::GUILD_EMOJIS_AND_STICKERS | GatewayIntents::GUILD_MEMBERS | GatewayIntents::GUILD_MESSAGE_REACTIONS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; if *CARG_NO_DISCORD.get().unwrap() { warn!("ABORTING CONNECTING TO DISCORD, BYE BYE!"); } else { let mut client = match Client::builder(&token, intents) .event_handler(BotHandler) .await { Ok(client) => client, Err(err) => panic!("Error starting client connection: `{err}`"), }; SHARD_MANAGER.set(client.shard_manager.clone()).unwrap(); if let Err(why) = client.start_shards(2).await { error!("Client error: {why:?}"); } } warn!("BOT EXITING"); }