diff options
| -rw-r--r-- | bin/main.rs | 10 | ||||
| -rw-r--r-- | src/lib.rs | 269 | ||||
| -rw-r--r-- | src/plugin.rs | 120 |
3 files changed, 238 insertions, 161 deletions
diff --git a/bin/main.rs b/bin/main.rs index 86910c0..f6a9418 100644 --- a/bin/main.rs +++ b/bin/main.rs @@ -4,6 +4,8 @@ extern crate time; use log::{LogRecord, LogLevel, LogLevelFilter, LogMetadata}; +use frippy::plugins; + struct Logger; impl log::Log for Logger { @@ -43,5 +45,11 @@ fn main() { }) .unwrap(); - frippy::run(); + let mut bot = frippy::Bot::new(); + + bot.add_plugin(plugins::Help::new()); + bot.add_plugin(plugins::Emoji::new()); + bot.add_plugin(plugins::Currency::new()); + + bot.run(); } @@ -4,11 +4,17 @@ //! Frippy is an IRC bot that runs plugins on each message //! received. //! -//! ## Example +//! ## Examples //! ```no_run -//! extern crate frippy; +//! use frippy::plugins; //! -//! frippy::run(); +//! let mut bot = frippy::Bot::new(); +//! +//! bot.add_plugin(plugins::Help::new()); +//! bot.add_plugin(plugins::Emoji::new()); +//! bot.add_plugin(plugins::Currency::new()); +//! +//! bot.run(); //! ``` //! //! # Logging @@ -25,9 +31,12 @@ extern crate tokio_core; extern crate futures; extern crate glob; -mod plugin; -mod plugins; +pub mod plugin; +pub mod plugins; +use std::fmt; +use std::collections::HashMap; +use std::thread::spawn; use std::sync::Arc; use irc::client::prelude::*; @@ -39,75 +48,114 @@ use glob::glob; use plugin::*; -/// Runs the bot -/// -/// # Remarks -/// -/// This blocks the current thread while the bot is running -pub fn run() { - - // 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), - } +pub struct Bot { + plugins: ThreadedPlugins, +} + +impl Bot { + /// Creates a `Bot`. + /// By itself the bot only responds to a few simple ctcp commands + /// defined per config file. + /// Any other functionality has to be provided by plugins + /// which need to implement [`Plugin`](plugin/trait.Plugin.html). + /// + /// # Examples + /// ``` + /// use frippy::Bot; + /// let mut bot = Bot::new(); + /// ``` + pub fn new() -> Bot { + Bot { plugins: ThreadedPlugins::new() } } - // Without configs the bot would just idle - if configs.is_empty() { - error!("No config file found"); - return; + /// Add plugins which should evaluate incoming messages from IRC. + /// + /// # Examples + /// ``` + /// use frippy::{plugins, Bot}; + /// + /// let mut bot = frippy::Bot::new(); + /// bot.add_plugin(plugins::Help::new()); + /// ``` + pub fn add_plugin<T: Plugin + 'static>(&mut self, plugin: T) { + self.plugins.add(plugin); } - // 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; + /// This starts the `Bot` which means that it tries + /// to create one connection for each toml file + /// found in the `configs` directory. + /// + /// Then it waits for incoming messages and sends them to the plugins. + /// This blocks the current thread until the `Bot` is shut down. + /// + /// # Examples + /// ```no_run + /// use frippy::{plugins, Bot}; + /// + /// let mut bot = Bot::new(); + /// bot.run(); + /// ``` + pub fn run(self) { + info!("Plugins loaded: {}", self.plugins); + + // 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), + } + } - info!("Connected to server"); + // Without configs the bot would just idle + if configs.is_empty() { + error!("No config file found"); + return; + } - match server.identify() { - Ok(_) => info!("Identified"), - Err(e) => error!("Failed to identify: {}", e), - }; + // Create an event loop to run the connections on. + let mut reactor = Core::new().unwrap(); - // TODO Verify if we actually need to clone plugins twice - let plugins = plugins.clone(); + // 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; + } + }; - let task = server - .stream() - .for_each(move |message| process_msg(&server, plugins.clone(), message)) - .map_err(|e| error!("Failed to process message: {}", e)); + info!("Connected to server"); - reactor.handle().spawn(task); - } + match server.identify() { + Ok(_) => info!("Identified"), + Err(e) => error!("Failed to identify: {}", e), + }; + + // TODO Verify if we actually need to clone plugins twice + let plugins = self.plugins.clone(); + + let task = server + .stream() + .for_each(move |message| process_msg(&server, plugins.clone(), message)) + .map_err(|e| error!("Failed to process message: {}", e)); - // Run the main loop forever - reactor.run(future::empty::<(), ()>()).unwrap(); + reactor.handle().spawn(task); + } + + // Run the main loop forever + reactor.run(future::empty::<(), ()>()).unwrap(); + } } fn process_msg(server: &IrcServer, @@ -115,6 +163,7 @@ fn process_msg(server: &IrcServer, message: Message) -> Result<(), IrcError> { + // Log any channels we join if let Command::JOIN(ref channel, _, _) = message.command { if message.source_nickname().unwrap() == server.current_nickname() { info!("Joined {}", channel); @@ -124,7 +173,6 @@ fn process_msg(server: &IrcServer, // Check for possible command and save the result for later let command = PluginCommand::from(&server.current_nickname().to_lowercase(), &message); - let message = Arc::new(message); plugins.execute_plugins(server, message); // If the message contained a command, handle it @@ -137,6 +185,99 @@ fn process_msg(server: &IrcServer, Ok(()) } +#[derive(Clone, Debug)] +struct ThreadedPlugins { + plugins: HashMap<String, Arc<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(plugin); + + self.plugins.insert(name, safe_plugin); + } + + pub fn execute_plugins(&mut self, server: &IrcServer, message: Message) { + let message = Arc::new(message); + + for (name, plugin) in self.plugins.clone() { + // Send the message to the plugin if the plugin needs it + if plugin.is_allowed(server, &message) { + + debug!("Executing {} with {}", + name, + message.to_string().replace("\r\n", "")); + + // 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) = 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); + + debug!("Sending command \"{:?}\" to {}", command, name); + + // 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) = 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)| p.name().to_string()) + .collect::<Vec<String>>(); + write!(f, "{}", plugin_names.join(", ")) + } +} #[cfg(test)] mod tests {} diff --git a/src/plugin.rs b/src/plugin.rs index d1f849a..0287989 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,29 +1,50 @@ use std::fmt; -use std::collections::HashMap; -use std::thread::spawn; -use std::sync::Arc; use irc::client::prelude::*; use irc::error::Error as IrcError; +/// `Plugin` has to be implemented for any struct that should be usable +/// as a plugin in frippy. pub trait Plugin: PluginName + Send + Sync + fmt::Debug { + /// This should return true if the `Plugin` wants to do work on the message. fn is_allowed(&self, server: &IrcServer, message: &Message) -> bool; + /// Handles messages which are not commands but still necessary. fn execute(&self, server: &IrcServer, message: &Message) -> Result<(), IrcError>; + /// Handles any command directed at this plugina. fn command(&self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError>; } +/// `PluginName` is required by `Plugin`. +/// To implement it simply add `#[derive(PluginName)]` +/// above the definition of the struct. +/// +/// # Examples +/// ```ignore +/// #[macro_use] extern crate plugin_derive; +/// +/// #[derive(PluginName)] +/// struct Foo; +/// ``` pub trait PluginName: Send + Sync + fmt::Debug { + /// Returns the name of the plugin. fn name(&self) -> &str; } +/// Represents a command sent by a user to the bot. #[derive(Clone, Debug)] pub struct PluginCommand { + /// The sender of the command. pub source: String, + /// If the command was sent to a channel, this will be that channel + /// otherwise it is the same as `source`. pub target: String, + /// The remaining part of the message that has not been processed yet - split by spaces. pub tokens: Vec<String>, } impl PluginCommand { + /// Creates a `PluginCommand` from `Message` if it is a `PRIVMSG` + /// that starts with the provided `nick`. pub fn from(nick: &str, message: &Message) -> Option<PluginCommand> { // Get the actual message out of PRIVMSG @@ -64,96 +85,3 @@ impl PluginCommand { } } } - -#[derive(Clone, Debug)] -pub struct ThreadedPlugins { - plugins: HashMap<String, Arc<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(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 plugin.is_allowed(server, &message) { - - debug!("Executing {} with {}", - name, - message.to_string().replace("\r\n", "")); - - // 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) = 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); - - debug!("Sending command \"{:?}\" to {}", command, name); - - // 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) = 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)| p.name().to_string()) - .collect::<Vec<String>>(); - write!(f, "{}", plugin_names.join(", ")) - } -} |
