diff options
| author | Jokler <jokler.contact@gmail.com> | 2017-11-28 03:12:48 +0100 |
|---|---|---|
| committer | Jokler <jokler.contact@gmail.com> | 2017-11-28 03:25:45 +0100 |
| commit | 09113bf4fa8cb8a42adb72533c3c76279e090978 (patch) | |
| tree | cfb65ad986dac30cf80f88ac36165dba6eab322d /src/lib.rs | |
| parent | 15e855ddecfdac31ddda26b12fcfd1a142a0ec21 (diff) | |
| download | frippy-09113bf4fa8cb8a42adb72533c3c76279e090978.tar.gz frippy-09113bf4fa8cb8a42adb72533c3c76279e090978.zip | |
Let users of the library define their own plugins
This means that:
- run() is now part of a Bot struct
- Plugins the bot should use have to be added before calling run()
- The Plugin trait and all of the included plugins are now public
Diffstat (limited to 'src/lib.rs')
| -rw-r--r-- | src/lib.rs | 269 |
1 files changed, 205 insertions, 64 deletions
@@ -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 {} |
