diff options
| author | Jokler <jokler.contact@gmail.com> | 2018-03-12 16:02:51 +0100 |
|---|---|---|
| committer | Jokler <jokler.contact@gmail.com> | 2018-03-12 16:02:51 +0100 |
| commit | 909cabe9280722e43c5fb283f768051bb85e1890 (patch) | |
| tree | 506ac34b7e22cdb95568cef9e649ee64cb3b0fdb /src/lib.rs | |
| parent | 15e855ddecfdac31ddda26b12fcfd1a142a0ec21 (diff) | |
| parent | 8e40e919aca8b8592be43e2c5bbcc0717bf14a6b (diff) | |
| download | frippy-909cabe9280722e43c5fb283f768051bb85e1890.tar.gz frippy-909cabe9280722e43c5fb283f768051bb85e1890.zip | |
Merge branch 'dev'
Diffstat (limited to 'src/lib.rs')
| -rw-r--r-- | src/lib.rs | 339 |
1 files changed, 252 insertions, 87 deletions
@@ -1,135 +1,196 @@ -#![cfg_attr(feature="clippy", feature(plugin))] -#![cfg_attr(feature="clippy", plugin(clippy))] +#![cfg_attr(feature = "clippy", feature(plugin))] +#![cfg_attr(feature = "clippy", plugin(clippy))] //! Frippy is an IRC bot that runs plugins on each message //! received. //! -//! ## Example +//! ## Examples //! ```no_run -//! extern crate frippy; +//! # extern crate irc; +//! # extern crate frippy; +//! # fn main() { +//! use frippy::{plugins, Config, Bot}; +//! use irc::client::reactor::IrcReactor; //! -//! frippy::run(); +//! let config = Config::load("config.toml").unwrap(); +//! let mut reactor = IrcReactor::new().unwrap(); +//! let mut bot = Bot::new(); +//! +//! bot.add_plugin(plugins::help::Help::new()); +//! bot.add_plugin(plugins::emoji::Emoji::new()); +//! bot.add_plugin(plugins::currency::Currency::new()); +//! +//! bot.connect(&mut reactor, &config).unwrap(); +//! reactor.run().unwrap(); +//! # } //! ``` //! //! # Logging //! Frippy uses the [log](https://docs.rs/log) crate so you can log events //! which might be of interest. +#[cfg(feature = "mysql")] #[macro_use] -extern crate log; +extern crate diesel; +#[cfg(feature = "mysql")] +extern crate r2d2; +#[cfg(feature = "mysql")] +extern crate r2d2_diesel; + +#[macro_use] +extern crate failure; +#[macro_use] +extern crate frippy_derive; #[macro_use] -extern crate plugin_derive; +extern crate lazy_static; +#[macro_use] +extern crate log; +extern crate chrono; +extern crate humantime; extern crate irc; -extern crate tokio_core; -extern crate futures; -extern crate glob; +extern crate reqwest; +extern crate time; -mod plugin; -mod plugins; +pub mod plugin; +pub mod plugins; +pub mod utils; +pub mod error; +use std::collections::HashMap; +use std::fmt; +use std::thread::spawn; use std::sync::Arc; -use irc::client::prelude::*; -use irc::error::Error as IrcError; - -use tokio_core::reactor::Core; -use futures::future; -use glob::glob; +pub use irc::client::prelude::*; +pub use irc::error::IrcError; +use error::*; +use failure::ResultExt; 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), +/// The bot which contains the main logic. +#[derive(Default)] +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; + /// Adds the [`Plugin`](plugin/trait.Plugin.html). + /// These plugins will be used to evaluate incoming messages from IRC. + /// + /// # Examples + /// ``` + /// use frippy::{plugins, Bot}; + /// + /// let mut bot = frippy::Bot::new(); + /// bot.add_plugin(plugins::help::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; - } - }; + /// Removes a [`Plugin`](plugin/trait.Plugin.html) based on its name. + /// The binary currently uses this to disable plugins + /// based on user configuration. + /// + /// # Examples + /// ``` + /// use frippy::{plugins, Bot}; + /// + /// let mut bot = frippy::Bot::new(); + /// bot.add_plugin(plugins::help::Help::new()); + /// bot.remove_plugin("Help"); + /// ``` + pub fn remove_plugin(&mut self, name: &str) -> Option<()> { + self.plugins.remove(name) + } - info!("Connected to server"); + /// This connects the `Bot` to IRC and creates a task on the + /// [`IrcReactor`](../irc/client/reactor/struct.IrcReactor.html) + /// which returns an Ok if the connection was cleanly closed and + /// an Err if the connection was lost. + /// + /// You need to run the [`IrcReactor`](../irc/client/reactor/struct.IrcReactor.html), + /// so that the `Bot` + /// can actually do its work. + /// + /// # Examples + /// ```no_run + /// # extern crate irc; + /// # extern crate frippy; + /// # fn main() { + /// use frippy::{Config, Bot}; + /// use irc::client::reactor::IrcReactor; + /// + /// let config = Config::load("config.toml").unwrap(); + /// let mut reactor = IrcReactor::new().unwrap(); + /// let mut bot = Bot::new(); + /// + /// bot.connect(&mut reactor, &config).unwrap(); + /// reactor.run().unwrap(); + /// # } + /// ``` + pub fn connect(&self, reactor: &mut IrcReactor, config: &Config) -> Result<(), FrippyError> { + info!("Plugins loaded: {}", self.plugins); - match server.identify() { - Ok(_) => info!("Identified"), - Err(e) => error!("Failed to identify: {}", e), - }; + let client = reactor + .prepare_client_and_connect(config) + .context(ErrorKind::Connection)?; + + info!("Connected to IRC server"); + + client.identify().context(ErrorKind::Connection)?; + info!("Identified"); // TODO Verify if we actually need to clone plugins twice - let plugins = plugins.clone(); + 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)); + reactor.register_client_with_handler(client, move |client, message| { + process_msg(client, plugins.clone(), message) + }); - reactor.handle().spawn(task); + Ok(()) } - - // Run the main loop forever - reactor.run(future::empty::<(), ()>()).unwrap(); } -fn process_msg(server: &IrcServer, - mut plugins: ThreadedPlugins, - message: Message) - -> Result<(), IrcError> { - +fn process_msg( + client: &IrcClient, + mut plugins: ThreadedPlugins, + 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() { + if message.source_nickname().unwrap() == client.current_nickname() { info!("Joined {}", channel); } } // Check for possible command and save the result for later - let command = PluginCommand::from(&server.current_nickname().to_lowercase(), &message); + let command = PluginCommand::from(&client.current_nickname().to_lowercase(), &message); - let message = Arc::new(message); - plugins.execute_plugins(server, message); + plugins.execute_plugins(client, message); // If the message contained a command, handle it if let Some(command) = command { - if let Err(e) = plugins.handle_command(server, command) { + if let Err(e) = plugins.handle_command(client, command) { error!("Failed to handle command: {}", e); } } @@ -137,6 +198,110 @@ fn process_msg(server: &IrcServer, Ok(()) } +#[derive(Clone, Default, 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); -#[cfg(test)] -mod tests {} + self.plugins.insert(name, safe_plugin); + } + + pub fn remove(&mut self, name: &str) -> Option<()> { + self.plugins.remove(&name.to_lowercase()).map(|_| ()) + } + + pub fn execute_plugins(&mut self, client: &IrcClient, 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 + match plugin.execute(client, &message) { + ExecutionStatus::Done => (), + ExecutionStatus::Err(e) => log_error(e), + ExecutionStatus::RequiresThread => { + debug!( + "Spawning thread to execute {} with {}", + name, + message.to_string().replace("\r\n", "") + ); + + // Clone everything before the move - the client uses an Arc internally too + let plugin = Arc::clone(&plugin); + let message = Arc::clone(&message); + let client = client.clone(); + + // Execute the plugin in another thread + spawn(move || { + if let Err(e) = plugin.execute_threaded(&client, &message) { + log_error(e); + }; + }); + } + } + } + } + + pub fn handle_command( + &mut self, + client: &IrcClient, + mut command: PluginCommand, + ) -> Result<(), FrippyError> { + if !command.tokens.iter().any(|s| !s.is_empty()) { + let help = format!("Use \"{} help\" to get help", client.current_nickname()); + client + .send_notice(&command.source, &help) + .context(ErrorKind::Connection)?; + } + + // 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 client uses an Arc internally + let client = client.clone(); + let plugin = Arc::clone(plugin); + spawn(move || { + if let Err(e) = plugin.command(&client, command) { + log_error(e); + }; + }); + + Ok(()) + } else { + let help = format!( + "\"{} {}\" is not a command, \ + try \"{0} help\" instead.", + client.current_nickname(), + command.tokens[0] + ); + + Ok(client + .send_notice(&command.source, &help) + .context(ErrorKind::Connection)?) + } + } +} + +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_owned()) + .collect::<Vec<String>>(); + write!(f, "{}", plugin_names.join(", ")) + } +} |
