diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib.rs | 210 | ||||
| -rw-r--r-- | src/plugin.rs | 140 | ||||
| -rw-r--r-- | src/plugins/currency.rs | 58 | ||||
| -rw-r--r-- | src/plugins/emoji.rs | 74 | ||||
| -rw-r--r-- | src/plugins/help.rs | 34 | ||||
| -rw-r--r-- | src/plugins/mod.rs | 9 |
6 files changed, 352 insertions, 173 deletions
@@ -4,42 +4,40 @@ //! Frippy is an IRC bot that runs plugins on each message //! received. //! -//! # Example +//! ## Example //! ```no_run //! extern crate frippy; //! //! frippy::run(); //! ``` +//! +//! # Logging +//! Frippy uses the [log](https://docs.rs/log) crate so you can log events +//! which might be of interest. #[macro_use] -extern crate lazy_static; +extern crate log; #[macro_use] extern crate plugin_derive; extern crate irc; -extern crate regex; +extern crate tokio_core; +extern crate futures; +extern crate glob; mod plugin; mod plugins; -use std::thread::spawn; -use std::sync::{Arc, Mutex}; -use regex::Regex; +use std::sync::Arc; + use irc::client::prelude::*; -use irc::proto::Command::PRIVMSG; use irc::error::Error as IrcError; -use plugin::*; +use tokio_core::reactor::Core; +use futures::future; +use glob::glob; -// Lock the mutex and ignore if it is poisoned -macro_rules! lock_plugin { - ($e:expr) => { - match $e.lock() { - Ok(plugin) => plugin, - Err(poisoned) => poisoned.into_inner(), - } - } -} +use plugin::*; /// Runs the bot /// @@ -47,136 +45,98 @@ macro_rules! lock_plugin { /// /// This blocks the current thread while the bot is running pub fn run() { - let server = IrcServer::new("config.toml").unwrap(); - server.identify().unwrap(); - // The list of plugins in use - let plugins: Vec<Arc<Mutex<Plugin>>> = - vec![Arc::new(Mutex::new(plugins::emoji::Emoji::new())), - Arc::new(Mutex::new(plugins::currency::Currency::new()))]; - - // We need the plugins' names to make sure the user gets a response - // if they use an incorrect plugin name - let plugin_names: Vec<String> = plugins - .iter() - .map(|p| p.lock().unwrap().name().to_lowercase()) - .collect(); - - // The main loop over received messages - server - .for_each_incoming(|message| { - let message = Arc::new(message); - // Check for possible command and save the result for later - let command = get_command(&server.current_nickname().to_lowercase(), &message); - - // Check if the first token of the command is valid - if let Some(ref c) = command { - if c.tokens.is_empty() { - let help = format!("Use \"{} help\" to get help", server.current_nickname()); - server.send_notice(&c.source, &help).unwrap(); - - } else if "help" == &c.tokens[0].to_lowercase() { - send_help_message(&server, c).unwrap(); - - } else if !plugin_names.contains(&c.tokens[0].to_lowercase()) { - - let help = format!("\"{} {}\" is not a command, \ - try \"{0} help\" instead.", - server.current_nickname(), - c.tokens[0]); - - server.send_notice(&c.source, &help).unwrap(); + // Load all toml files in the configs directory + let mut configs = Vec::new(); + for toml in glob("configs/*.toml").unwrap() { + match toml { + Ok(path) => { + info!("Loading {}", path.to_str().unwrap()); + match Config::load(path) { + Ok(v) => configs.push(v), + Err(e) => error!("Incorrect config file {}", e), } } + Err(e) => error!("Failed to read path {}", e), + } + } - for plugin in plugins.clone() { - // Send the message to the plugin if the plugin needs it - if lock_plugin!(plugin).is_allowed(&server, &message) { - - // Clone everything before the move - // The server uses an Arc internally too - let plugin = Arc::clone(&plugin); - let message = Arc::clone(&message); - let server = server.clone(); + // Without configs the bot would just idle + if configs.is_empty() { + error!("No config file found"); + return; + } - // Execute the plugin in another thread - spawn(move || { lock_plugin!(plugin).execute(&server, &message).unwrap(); }); + // The list of plugins in use + let mut plugins = ThreadedPlugins::new(); + plugins.add(plugins::Help::new()); + plugins.add(plugins::Emoji::new()); + plugins.add(plugins::Currency::new()); + info!("Plugins loaded: {}", plugins); + + // Create an event loop to run the connections on. + let mut reactor = Core::new().unwrap(); + + // Open a connection and add work for each config + for config in configs { + let server = + match IrcServer::new_future(reactor.handle(), &config).and_then(|f| reactor.run(f)) { + Ok(v) => v, + Err(e) => { + error!("Failed to connect: {}", e); + return; } + }; - // Check if the command is for this plugin - // Clone it for the move - if let Some(mut c) = command.clone() { + info!("Connected to server"); - // Skip empty commands - if c.tokens.is_empty() { continue; } + match server.identify() { + Ok(_) => info!("Identified"), + Err(e) => error!("Failed to identify: {}", e), + }; - if lock_plugin!(plugin).name().to_lowercase() == c.tokens[0].to_lowercase() { + // TODO Verify if we actually need to clone plugins twice + let plugins = plugins.clone(); - // The first token contains the name of the plugin - c.tokens.remove(0); + let task = server + .stream() + .for_each(move |message| process_msg(&server, plugins.clone(), message)) + .map_err(|e| error!("Failed to process message: {}", e)); - // Clone the server for the move - it uses an Arc internally - let server = server.clone(); - spawn(move || { lock_plugin!(plugin).command(&server, c).unwrap(); }); - } - } - } - }) - .unwrap(); -} + reactor.handle().spawn(task); + } -fn send_help_message(server: &IrcServer, command: &PluginCommand) -> Result<(), IrcError> { - server.send_notice(&command.source, "Help has not been added yet.") + // Run the main loop forever + reactor.run(future::empty::<(), ()>()).unwrap(); } -fn get_command(nick: &str, message: &Message) -> Option<PluginCommand> { - - // Get the actual message out of PRIVMSG - if let PRIVMSG(_, ref content) = message.command { - - // Split content by spaces and filter empty tokens - let mut tokens: Vec<String> = content - .split(' ') - .filter(|&x| !x.is_empty()) - .map(ToOwned::to_owned) - .collect(); - - // Check if the message contained notthing but spaces - if tokens.is_empty() { - return None; - } +fn process_msg(server: &IrcServer, + mut plugins: ThreadedPlugins, + message: Message) + -> Result<(), IrcError> { - // Only compile the regex once - // We assume that only ':' and ',' are used as suffixes on IRC - lazy_static! { - static ref RE: Regex = Regex::new("^[:,]*?$").unwrap(); + if let Command::JOIN(ref channel, _, _) = message.command { + if message.source_nickname().unwrap() == server.current_nickname() { + info!("Joined {}", channel); } + } - if tokens[0].to_lowercase().starts_with(nick) { - - // Remove the bot's name from the first token - tokens[0].drain(..nick.len()); - - // If the regex does not match the message is not directed at the bot - if !RE.is_match(&tokens[0]) { - return None; - } + // Check for possible command and save the result for later + let command = PluginCommand::from(&server.current_nickname().to_lowercase(), &message); - // The first token contained the name of the bot - tokens.remove(0); + let message = Arc::new(message); + plugins.execute_plugins(server, message); - Some(PluginCommand { - source: message.source_nickname().unwrap().to_string(), - target: message.response_target().unwrap().to_string(), - tokens: tokens, - }) - } else { - None + // If the message contained a command, handle it + if let Some(command) = command { + if let Err(e) = plugins.handle_command(server, command) { + error!("Failed to handle command: {}", e); } - } else { - None } + + Ok(()) } + #[cfg(test)] mod tests {} diff --git a/src/plugin.rs b/src/plugin.rs index 0a4034d..e0a4ce2 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,4 +1,8 @@ use std::fmt; +use std::collections::HashMap; +use std::thread::spawn; +use std::sync::{Arc, Mutex}; + use irc::client::prelude::*; use irc::error::Error as IrcError; @@ -18,3 +22,139 @@ pub struct PluginCommand { pub target: String, pub tokens: Vec<String>, } + +impl PluginCommand { + pub fn from(nick: &str, message: &Message) -> Option<PluginCommand> { + + // Get the actual message out of PRIVMSG + if let Command::PRIVMSG(_, ref content) = message.command { + + // Split content by spaces and filter empty tokens + let mut tokens: Vec<String> = content.split(' ').map(ToOwned::to_owned).collect(); + + // Commands start with our name + if tokens[0].to_lowercase().starts_with(nick) { + + // Remove the bot's name from the first token + tokens[0].drain(..nick.len()); + + // We assume that only ':' and ',' are used as suffixes on IRC + // If there are any other chars we assume that it is not ment for the bot + tokens[0] = tokens[0] + .chars() + .filter(|&c| !":,".contains(c)) + .collect(); + if !tokens[0].is_empty() { + return None; + } + + // The first token contained the name of the bot + tokens.remove(0); + + Some(PluginCommand { + source: message.source_nickname().unwrap().to_string(), + target: message.response_target().unwrap().to_string(), + tokens: tokens, + }) + } else { + None + } + } else { + None + } + } +} + +// Lock the mutex and ignore if it is poisoned +macro_rules! lock_plugin { + ($e:expr) => { + match $e.lock() { + Ok(plugin) => plugin, + Err(poisoned) => poisoned.into_inner(), + } + } +} + +#[derive(Clone, Debug)] +pub struct ThreadedPlugins { + plugins: HashMap<String, Arc<Mutex<Plugin>>>, +} + +impl ThreadedPlugins { + pub fn new() -> ThreadedPlugins { + ThreadedPlugins { plugins: HashMap::new() } + } + + pub fn add<T: Plugin + 'static>(&mut self, plugin: T) { + let name = plugin.name().to_lowercase(); + let safe_plugin = Arc::new(Mutex::new(plugin)); + + self.plugins.insert(name, safe_plugin); + } + + pub fn execute_plugins(&mut self, server: &IrcServer, message: Arc<Message>) { + + for (name, plugin) in self.plugins.clone() { + // Send the message to the plugin if the plugin needs it + if lock_plugin!(plugin).is_allowed(server, &message) { + + // Clone everything before the move + // The server uses an Arc internally too + let plugin = Arc::clone(&plugin); + let message = Arc::clone(&message); + let server = server.clone(); + + // Execute the plugin in another thread + spawn(move || { + if let Err(e) = lock_plugin!(plugin).execute(&server, &message) { + error!("Error in {} - {}", name, e); + }; + }); + } + } + } + + pub fn handle_command(&mut self, server: &IrcServer, mut command: PluginCommand) -> Result<(), IrcError> { + + if !command.tokens.iter().any(|s| !s.is_empty()) { + let help = format!("Use \"{} help\" to get help", server.current_nickname()); + return server.send_notice(&command.source, &help); + } + + // Check if the command is for this plugin + if let Some(plugin) = self.plugins.get(&command.tokens[0].to_lowercase()) { + + // The first token contains the name of the plugin + let name = command.tokens.remove(0); + + // Clone for the move - the server uses an Arc internally + let server = server.clone(); + let plugin = Arc::clone(plugin); + spawn(move || { + if let Err(e) = lock_plugin!(plugin).command(&server, command) { + error!("Error in {} command - {}", name, e); + }; + }); + + Ok(()) + + } else { + let help = format!("\"{} {}\" is not a command, \ + try \"{0} help\" instead.", + server.current_nickname(), + command.tokens[0]); + + server.send_notice(&command.source, &help) + } + } +} + +impl fmt::Display for ThreadedPlugins { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let plugin_names = self.plugins + .iter() + .map(|(_, p)| lock_plugin!(p).name().to_string()) + .collect::<Vec<String>>(); + write!(f, "{}", plugin_names.join(", ")) + } +} diff --git a/src/plugins/currency.rs b/src/plugins/currency.rs index bb16cd9..78ae593 100644 --- a/src/plugins/currency.rs +++ b/src/plugins/currency.rs @@ -4,8 +4,11 @@ extern crate serde_json; extern crate regex; use std::io::Read; +use std::num::ParseFloatError; + use irc::client::prelude::*; use irc::error::Error as IrcError; + use self::reqwest::Client; use self::reqwest::header::Connection; use self::serde_json::Value; @@ -66,25 +69,23 @@ impl Currency { Currency {} } - fn eval_command<'a>(&self, tokens: &'a [String]) -> Option<ConvertionRequest<'a>> { - let parsed = match tokens[0].parse() { - Ok(v) => v, - Err(_) => { - return None; - } - }; - - Some(ConvertionRequest { - value: parsed, - source: &tokens[1], - target: &tokens[2], - }) + fn eval_command<'a>(&self, tokens: &'a [String]) -> Result<ConvertionRequest<'a>, ParseFloatError> { + Ok(ConvertionRequest { + value: tokens[0].parse()?, + source: &tokens[1], + target: &tokens[2], + }) } fn convert(&self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError> { + + if command.tokens.len() < 3 { + return self.invalid_command(server, &command); + } + let request = match self.eval_command(&command.tokens) { - Some(request) => request, - None => { + Ok(request) => request, + Err(_) => { return self.invalid_command(server, &command); } }; @@ -103,7 +104,7 @@ impl Currency { } } - fn help(&self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError> { + fn help(&self, server: &IrcServer, command: &mut PluginCommand) -> Result<(), IrcError> { let help = format!("usage: {} currency value from_currency to_currency\r\n\ example: 1.5 eur usd\r\n\ available currencies: AUD, BGN, BRL, CAD, \ @@ -111,14 +112,14 @@ impl Currency { IDR, ILS, INR, JPY, KRW, MXN, MYR, NOK, \ NZD, PHP, PLN, RON, RUB, SEK, SGD, THB, \ TRY, USD, ZAR", - server.current_nickname()); + server.current_nickname()); server.send_notice(&command.source, &help) } fn invalid_command(&self, server: &IrcServer, command: &PluginCommand) -> Result<(), IrcError> { - let help = format!("Incorrect value. \ - Send \"{} help currency\" for help.", + let help = format!("Incorrect Command. \ + Send \"{} currency help\" for help.", server.current_nickname()); server.send_notice(&command.source, &help) @@ -131,21 +132,18 @@ impl Plugin for Currency { } fn execute(&mut self, _: &IrcServer, _: &Message) -> Result<(), IrcError> { - Ok(()) + panic!("Currency does not implement the execute function!") } - fn command(&mut self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError> { - if command.tokens.is_empty() { - self.invalid_command(server, &command) + fn command(&mut self, server: &IrcServer, mut command: PluginCommand) -> Result<(), IrcError> { - } else if command.tokens[0].to_lowercase() == "help" { - self.help(server, command) - - } else if command.tokens.len() >= 3 { - self.convert(server, command) + if command.tokens.is_empty() { + return self.invalid_command(server, &command); + } - } else { - self.invalid_command(server, &command) + match command.tokens[0].as_ref() { + "help" => self.help(server, &mut command), + _ => self.convert(server, command), } } } diff --git a/src/plugins/emoji.rs b/src/plugins/emoji.rs index d2ed956..09d5c27 100644 --- a/src/plugins/emoji.rs +++ b/src/plugins/emoji.rs @@ -1,43 +1,83 @@ extern crate unicode_names; +use std::fmt; + use irc::client::prelude::*; use irc::error::Error as IrcError; use plugin::*; +struct EmojiHandle { + symbol: char, + count: i32, +} + +impl fmt::Display for EmojiHandle { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + + let name = match unicode_names::name(self.symbol) { + Some(sym) => sym.to_string().to_lowercase(), + None => String::from("UNKNOWN"), + }; + + if self.count > 1 { + write!(f, "{}x {}", self.count, name) + } else { + write!(f, "{}", name) + } + } +} + #[derive(PluginName, Debug)] pub struct Emoji; + impl Emoji { pub fn new() -> Emoji { Emoji {} } fn emoji(&self, server: &IrcServer, content: &str, target: &str) -> Result<(), IrcError> { - - let mut names: Vec<String> = Vec::new(); - for emoji in self.return_emojis(content) { - - let name = match unicode_names::name(emoji) { - Some(v) => format!("{}", v).to_lowercase(), - None => "UNKNOWN".to_string(), - }; - - names.push(name); - } + let names = self.return_emojis(content) + .iter() + .map(|e| e.to_string()) + .collect::<Vec<String>>(); server.send_privmsg(target, &names.join(", ")) } - fn return_emojis(&self, string: &str) -> Vec<char> { + fn return_emojis(&self, string: &str) -> Vec<EmojiHandle> { + let mut emojis: Vec<EmojiHandle> = Vec::new(); + + let mut current = EmojiHandle { + symbol: ' ', + count: 0, + }; - let mut emojis: Vec<char> = Vec::new(); for c in string.chars() { - if self.is_emoji(&c) { - emojis.push(c); + if !self.is_emoji(&c) { + continue; + } + + if current.symbol == c { + current.count += 1; + + } else { + if current.count > 0 { + emojis.push(current); + } + + current = EmojiHandle { + symbol: c, + count: 1, + } } } + if current.count > 0 { + emojis.push(current); + } + emojis } @@ -67,7 +107,9 @@ impl Plugin for Emoji { fn execute(&mut self, server: &IrcServer, message: &Message) -> Result<(), IrcError> { match message.command { - Command::PRIVMSG(ref target, ref content) => self.emoji(server, content, target), + Command::PRIVMSG(_, ref content) => { + self.emoji(server, content, message.response_target().unwrap()) + } _ => Ok(()), } } diff --git a/src/plugins/help.rs b/src/plugins/help.rs new file mode 100644 index 0000000..c4ddcd4 --- /dev/null +++ b/src/plugins/help.rs @@ -0,0 +1,34 @@ +use irc::client::prelude::*; +use irc::error::Error as IrcError; + +use plugin::*; + +#[derive(PluginName, Debug)] +pub struct Help; + +impl Help { + pub fn new() -> Help { + Help {} + } + + fn help(&self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError> { + server.send_notice(&command.source, "Help has not been added yet.") + } +} + +impl Plugin for Help { + fn is_allowed(&self, _: &IrcServer, _: &Message) -> bool { + false + } + + fn execute(&mut self, _: &IrcServer, _: &Message) -> Result<(), IrcError> { + panic!("Help does not implement the execute function!") + } + + fn command(&mut self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError> { + self.help(server, command) + } +} + +#[cfg(test)] +mod tests {} diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index adf54b2..0dea596 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -1,2 +1,7 @@ -pub mod emoji; -pub mod currency; +mod help; +mod emoji; +mod currency; + +pub use self::help::Help; +pub use self::emoji::Emoji; +pub use self::currency::Currency; |
