From b5c6a0e5a65d13c639830296400ceb176f64c716 Mon Sep 17 00:00:00 2001 From: Nathan Perry Date: Sat, 26 Sep 2020 14:56:53 -0400 Subject: reorganize modules --- src/bot.rs | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 10 +++ src/log.rs | 43 ------------ src/log_setup.rs | 43 ++++++++++++ src/main.rs | 191 ++------------------------------------------------- src/util.rs | 7 +- 6 files changed, 266 insertions(+), 233 deletions(-) create mode 100644 src/bot.rs delete mode 100644 src/log.rs create mode 100644 src/log_setup.rs diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..3923458 --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,205 @@ +use std::{ + sync::Mutex, + fs::File, + result::Result as StdResult, +}; + +use serenity::{ + prelude::*, + model::{ + gateway::Ready, + id::{ + ChannelId, + MessageId, + }, + channel::Message, + }, + framework::StandardFramework, +}; + +use fnv::{ + FnvHashMap, + FnvHashSet, +}; + +use chrono::Datelike; +use lazy_static::lazy_static; +use log::{ + debug, + info, + error, + trace, + warn, +}; + +use crate::{ + Result, + Error, + audio, + util::OAUTH_URL, + util::CtxExt, + commands::register_commands, + config::CONFIG, +}; + +struct Handler; +impl EventHandler for Handler { + fn ready(&self, ctx: Context, r: Ready) { + let guild = r.guilds.iter() + .find(|g| g.id() == CONFIG.discord.guild()); + + if guild.is_none() { + info!("bot isn't in configured guild. join here: {:?}", OAUTH_URL.as_str()); + } + + #[cfg(debug_assertions)] + let botname = "thulani (dev)"; + + #[cfg(not(debug_assertions))] + let botname = "thulani"; + + guild.iter().for_each(|g| { + if let Err(e) = g.id().edit_nickname(&ctx, Some(botname)) { + error!("changing nickname: {:?}", e); + } + }); + } + + fn message_delete(&self, _ctx: Context, _channel_id: ChannelId, deleted_message_id: MessageId) { + MESSAGE_WATCH.lock() + .unwrap() + .remove(&deleted_message_id); + } +} + +lazy_static! { + static ref MESSAGE_WATCH: Mutex> = Mutex::new(FnvHashMap::default()); + static ref PREFIXES: Vec<&'static str> = vec!["!thulani ", "!thulan ", "!thulando madando ", "!thulando "]; + static ref RESTRICTED_PREFIXES: Vec<&'static str> = vec!["!todd ", "!toddbert ", "!toddlani "]; + static ref ALL_PREFIXES: Vec<&'static str> = { + let mut all_prefixes: Vec<&'static str> = vec![]; + all_prefixes.extend(PREFIXES.iter()); + all_prefixes.extend(RESTRICTED_PREFIXES.iter()); + all_prefixes + }; + + static ref RESTRICT_IDS: FnvHashSet = { + let restrict_ids = File::open("restrict.json") + .map_err(Error::from) + .and_then(|f| serde_json::from_reader::<_, Vec>(f).map_err(Error::from)); + + if let Err(ref e) = restrict_ids { + warn!("opening restrict file: {}", e); + } + + restrict_ids + .unwrap_or_default() + .into_iter() + .collect::>() + }; +} + +fn framework() -> StandardFramework { + let framework = StandardFramework::new() + .configure(|c| c + .allow_dm(false) + .with_whitespace(true) + .prefixes(ALL_PREFIXES.iter()) + .ignore_bots(true) + .on_mention(None) + .owners(vec![CONFIG.discord.owner()].into_iter().collect()) + .case_insensitivity(true) + ) + .before(before_handle) + .after(after_handle) + .bucket("Standard", |b| { + b.delay(1).limit(20).time_span(60) + }); + + register_commands(framework) +} + +fn before_handle(ctx: &mut Context, message: &Message, cmd: &str) -> bool { + debug!("got command '{}' from user '{}' ({})", cmd, message.author.name, message.author.id); + + if !message.guild_id.map_or(false, |x| x == CONFIG.discord.guild()) { + info!("rejecting command '{}' from user '{}': wrong guild", cmd, message.author.name); + return false; + } + + if message.author.id == CONFIG.discord.owner() { + return true; + } + + let restricted_prefix = RESTRICTED_PREFIXES.iter() + .any(|prefix| message.content.starts_with(prefix)); + + if !restricted_prefix { + return true; + } + + const PERMITTED_WEEKDAY: chrono::Weekday = chrono::Weekday::Tue; + + let user_is_restricted = RESTRICT_IDS.contains(&message.author.id.0); + let restrictions_flipped = chrono::Local::now().weekday() == PERMITTED_WEEKDAY; + + if user_is_restricted == restrictions_flipped { + return true; + } + + let reason = if !restrictions_flipped { + "restricted prefix".to_owned() + } else { + format!("it is {:?}", PERMITTED_WEEKDAY) + }; + + info!("rejecting command '{}' from user '{}': {}", cmd, message.author.name, reason); + + match ctx.send_result(message.channel_id, "no", message.tts) { + Err(e) => error!("sending restricted prefix response: {}", e), + Ok(msg_id) => { + let mut mp = MESSAGE_WATCH.lock().unwrap(); + mp.insert(message.id, msg_id); + } + } + + false +} + +fn after_handle(ctx: &mut Context, msg: &Message, cmd: &str, err: StdResult<(), Error>) { + match err { + Ok(()) => { + trace!("command '{}' completed successfully", cmd); + }, + + Err(e) => { + if let Err(e) = msg.react(&ctx, "❌") { + error!("reacting to failed message: {}", e); + } + + if let Err(e) = ctx.send(msg.channel_id, "BANIC", msg.tts) { + error!("sending BANIC: {}", e); + } + + error!("error encountered handling command '{}': {:?}", cmd, e); + } + } +} + +pub fn run() -> Result<()> { + let token = &CONFIG.discord.auth.token; + let mut client = Client::new(token, Handler)?; + + audio::VoiceManager::register(&mut client); + audio::PlayQueue::register(&mut client); + + client.with_framework(framework()); + + let shard_manager = client.shard_manager.clone(); + ctrlc::set_handler(move || { + info!("shutting down"); + shard_manager.lock().shutdown_all(); + }).expect("unable to create SIGINT/SIGTERM handlers"); + + client.start() +} diff --git a/src/config.rs b/src/config.rs index 13d015e..53801b6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,8 +6,18 @@ use serenity::{ }, }; +use dotenv::dotenv; +use lazy_static::lazy_static; use envconfig::Envconfig; +lazy_static! { + pub static ref CONFIG: Config = { + dotenv().ok(); + + Config::init().unwrap() + }; +} + #[derive(Envconfig)] pub struct Config { #[envconfig(from = "DATABASE_URL")] diff --git a/src/log.rs b/src/log.rs deleted file mode 100644 index a51ebcc..0000000 --- a/src/log.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::{Result, Error}; - -use fern::colors::{Color, ColoredLevelConfig}; - -pub fn init() -> Result<()> { - let colors = ColoredLevelConfig::new() - .info(Color::Green) - .debug(Color::BrightBlue) - .trace(Color::BrightMagenta); - - fern::Dispatch::new() - .level_for("serenity::voice::connection", log::LevelFilter::Error) - .chain(fern::Dispatch::new() - .format(move |out, message, record| { - out.finish(format_args!( - "{} [{}] [{}] {}", - chrono::Local::now().format("%_m/%_d/%y %l:%M:%S%P"), - colors.color(record.level()), - record.target(), - message - )) - }) - .level(log::LevelFilter::Warn) - .level_for("thulani", log::LevelFilter::Debug) - .chain(std::io::stdout()) - ) - .chain(fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} [{}] [{}] {}", - chrono::Local::now().format("%_m/%_d/%y %l:%M:%S%P"), - record.level(), - record.target(), - message - )) - }) - .level(log::LevelFilter::Info) - .level_for("thulani", log::LevelFilter::Trace) - .chain(fern::log_file("thulani.log").expect("problem creating log file")) - ) - .apply() - .map_err(Error::from) -} \ No newline at end of file diff --git a/src/log_setup.rs b/src/log_setup.rs new file mode 100644 index 0000000..a51ebcc --- /dev/null +++ b/src/log_setup.rs @@ -0,0 +1,43 @@ +use crate::{Result, Error}; + +use fern::colors::{Color, ColoredLevelConfig}; + +pub fn init() -> Result<()> { + let colors = ColoredLevelConfig::new() + .info(Color::Green) + .debug(Color::BrightBlue) + .trace(Color::BrightMagenta); + + fern::Dispatch::new() + .level_for("serenity::voice::connection", log::LevelFilter::Error) + .chain(fern::Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "{} [{}] [{}] {}", + chrono::Local::now().format("%_m/%_d/%y %l:%M:%S%P"), + colors.color(record.level()), + record.target(), + message + )) + }) + .level(log::LevelFilter::Warn) + .level_for("thulani", log::LevelFilter::Debug) + .chain(std::io::stdout()) + ) + .chain(fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} [{}] [{}] {}", + chrono::Local::now().format("%_m/%_d/%y %l:%M:%S%P"), + record.level(), + record.target(), + message + )) + }) + .level(log::LevelFilter::Info) + .level_for("thulani", log::LevelFilter::Trace) + .chain(fern::log_file("thulani.log").expect("problem creating log file")) + ) + .apply() + .map_err(Error::from) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index daafbf4..6c2d651 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,6 @@ #[macro_use] extern crate envconfig_derive; use std::{ - default::Default, - fs::File, thread, time::{ Duration, @@ -21,29 +19,10 @@ use std::{ }, }; -use chrono::Datelike; -use fnv::{FnvHashMap, FnvHashSet}; use log::{ - debug, error, info, - trace, - warn, }; -use serenity::{ - framework::StandardFramework, - model::{ - gateway::Ready, - id::{ChannelId, MessageId}, - }, - prelude::*, -}; - -use dotenv::dotenv; -use lazy_static::lazy_static; -use envconfig::Envconfig; - -use self::commands::register_commands; pub use self::util::*; pub use self::config::*; @@ -68,174 +47,12 @@ mod commands; mod util; mod audio; mod config; -mod log; +mod log_setup; +mod bot; pub type Error = anyhow::Error; - pub type Result = anyhow::Result; -lazy_static! { - pub static ref CONFIG: Config = { - dotenv().ok(); - - Config::init().unwrap() - }; -} - -struct Handler; -impl EventHandler for Handler { - fn ready(&self, ctx: Context, r: Ready) { - let guild = r.guilds.iter() - .find(|g| g.id() == CONFIG.discord.guild()); - - if guild.is_none() { - info!("bot isn't in configured guild. join here: {:?}", OAUTH_URL.as_str()); - } - - #[cfg(debug_assertions)] { - let _ = guild.map(|g| g.id().edit_nickname(ctx, Some("thulani (dev)"))); - } - - #[cfg(not(debug_assertions))] { - let _ = guild.map(|g| g.id().edit_nickname(ctx, Some("thulani"))); - } - } - - fn message_delete(&self, ctx: Context, channel_id: ChannelId, deleted_message_id: MessageId) { - MESSAGE_WATCH.lock() - .remove(&deleted_message_id) - .iter() - .for_each(|id| { - if let Err(e) = channel_id.delete_message(&ctx, id) { - error!("deleting message: {}", e); - } - }); - } -} - -lazy_static! { - static ref MESSAGE_WATCH: Mutex> = Mutex::new(FnvHashMap::default()); - static ref PREFIXES: Vec<&'static str> = vec!["!thulani ", "!thulan ", "!thulando madando ", "!thulando "]; - static ref RESTRICTED_PREFIXES: Vec<&'static str> = vec!["!todd ", "!toddbert ", "!toddlani "]; -} - - -fn run() -> Result<()> { - let token = &CONFIG.discord.auth.token; - let mut client = Client::new(token, Handler)?; - - audio::VoiceManager::register(&mut client); - audio::PlayQueue::register(&mut client); - - let all_prefixes = { - let mut all_prefixes: Vec<&'static str> = vec![]; - all_prefixes.extend(PREFIXES.iter()); - all_prefixes.extend(RESTRICTED_PREFIXES.iter()); - all_prefixes - }; - - let restrict_ids = File::open("restrict.json") - .map_err(Error::from) - .and_then(|f| serde_json::from_reader::<_, Vec>(f).map_err(Error::from)); - - if let Err(ref e) = restrict_ids { - warn!("opening restrict file: {}", e); - } - - let restrict_ids = restrict_ids - .unwrap_or_default() - .into_iter() - .collect::>(); - - let mut framework = StandardFramework::new() - .configure(|c| c - .allow_dm(false) - .with_whitespace(true) - .prefixes(all_prefixes) - .ignore_bots(true) - .on_mention(None) - .owners(vec![CONFIG.discord.owner()].into_iter().collect()) - .case_insensitivity(true) - ) - .before(move |ctx, message, cmd| { - debug!("got command '{}' from user '{}' ({})", cmd, message.author.name, message.author.id); - if !message.guild_id.map_or(false, |x| x == CONFIG.discord.guild()) { - info!("rejecting command '{}' from user '{}': wrong guild", cmd, message.author.name); - return false; - } - - if message.author.id == CONFIG.discord.owner() { - return true; - } - - let restricted_prefix = RESTRICTED_PREFIXES.iter().any(|prefix| message.content.starts_with(prefix)); - if !restricted_prefix { - return true; - } - - const PERMITTED_WEEKDAY: chrono::Weekday = chrono::Weekday::Tue; - - let restricted_user = restrict_ids.contains(&message.author.id.0); - let flip_restriction_day = chrono::Local::now().weekday() == PERMITTED_WEEKDAY; - - if restricted_user == flip_restriction_day { - return true; - } - - let reason = if !flip_restriction_day { - "restricted prefix".to_owned() - } else { - format!("it is {:?}", PERMITTED_WEEKDAY) - }; - - info!("rejecting command '{}' from user '{}': {}", cmd, message.author.name, reason); - - match ctx.send_result(message.channel_id, "no", message.tts) { - Err(e) => error!("sending restricted prefix response: {}", e), - Ok(msg_id) => { - let mut mp = MESSAGE_WATCH.lock(); - mp.insert(message.id, msg_id); - } - } - - return false; - }) - .after(|ctx, msg, cmd, err| { - match err { - Ok(()) => { - trace!("command '{}' completed successfully", cmd); - }, - - Err(e) => { - if let Err(e) = msg.react(&ctx, "❌") { - error!("reacting to failed message: {}", e); - } - - if let Err(e) = ctx.send(msg.channel_id, "BANIC", msg.tts) { - error!("sending BANIC: {}", e); - } - - error!("error encountered handling command '{}': {:?}", cmd, e); - } - } - }) - .bucket("Standard", |b| b.delay(1).limit(20).time_span(60)); - - framework = register_commands(framework); - - client.with_framework(framework); - - let shard_manager = client.shard_manager.clone(); - ctrlc::set_handler(move || { - info!("shutting down"); - shard_manager.lock().shutdown_all(); - }).expect("unable to create SIGINT/SIGTERM handlers"); - - client.start()?; - - Ok(()) -} - fn main() { const BACKOFF_FACTOR: f64 = 2.0; const MAX_BACKOFFS: usize = 3; @@ -245,7 +62,7 @@ fn main() { info!("starting"); - log::init().expect("initializing logging"); + log_setup::init().expect("initializing logging"); let mut backoff_count: usize = 0; @@ -253,7 +70,7 @@ fn main() { let start = Instant::now(); info!("starting bot"); - match run() { + match bot::run() { Err(e) => { error!("error encountered running client: {:?}", e); }, diff --git a/src/util.rs b/src/util.rs index 77f2dd4..e9b6203 100644 --- a/src/util.rs +++ b/src/util.rs @@ -8,9 +8,10 @@ use serenity::{ permissions::Permissions, } }; -use url::Url; +use url::Url; use lazy_static::lazy_static; +use log::debug; use crate::{ CONFIG, @@ -52,8 +53,8 @@ impl CtxExt for Context { #[inline] fn send_result>(&self, channel: ChannelId, text: A, tts: bool) -> Result { let text = text.as_ref(); - debug!("sending message {:?} to channel {:?}", text, channel); - let result = channel.send_message(self, |m| m.content(text.as_ref()).tts(tts))?; + debug!("sending message {:?} to channel {:?} (tts: {})", text, channel, tts); + let result = channel.send_message(self, |m| m.content(text).tts(tts))?; Ok(result.id) } } -- cgit v1.3.1