diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib.rs | 171 | ||||
| -rw-r--r-- | src/main.rs | 166 | ||||
| -rw-r--r-- | src/plugin.rs | 52 | ||||
| -rw-r--r-- | src/plugins/currency.rs | 115 | ||||
| -rw-r--r-- | src/plugins/emoji.rs | 51 | ||||
| -rw-r--r-- | src/plugins/factoids/mod.rs | 112 | ||||
| -rw-r--r-- | src/plugins/help.rs | 22 | ||||
| -rw-r--r-- | src/plugins/keepnick.rs | 53 | ||||
| -rw-r--r-- | src/plugins/mod.rs | 2 | ||||
| -rw-r--r-- | src/plugins/tell.rs | 140 | ||||
| -rw-r--r-- | src/plugins/url.rs | 59 |
11 files changed, 647 insertions, 296 deletions
@@ -1,29 +1,27 @@ -#![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. //! //! ## Examples //! ```no_run -//! # extern crate tokio_core; -//! # extern crate futures; +//! # extern crate irc; //! # extern crate frippy; //! # fn main() { //! use frippy::{plugins, Config, Bot}; -//! use tokio_core::reactor::Core; -//! use futures::future; +//! use irc::client::reactor::IrcReactor; //! //! let config = Config::load("config.toml").unwrap(); -//! let mut reactor = Core::new().unwrap(); +//! let mut reactor = IrcReactor::new().unwrap(); //! let mut bot = Bot::new(); //! //! bot.add_plugin(plugins::Help::new()); //! bot.add_plugin(plugins::Emoji::new()); //! bot.add_plugin(plugins::Currency::new()); //! -//! bot.connect(&mut reactor, &config); -//! reactor.run(future::empty::<(), ()>()).unwrap(); +//! bot.connect(&mut reactor, &config).unwrap(); +//! reactor.run().unwrap(); //! # } //! ``` //! @@ -36,16 +34,13 @@ extern crate diesel; #[macro_use] -extern crate log; +extern crate frippy_derive; #[macro_use] extern crate lazy_static; #[macro_use] -extern crate frippy_derive; +extern crate log; extern crate irc; -extern crate futures; -extern crate tokio_core; -extern crate regex; extern crate chrono; extern crate time; @@ -58,9 +53,8 @@ use std::fmt; use std::thread::spawn; use std::sync::Arc; -use tokio_core::reactor::Core; pub use irc::client::prelude::*; -pub use irc::error::Error as IrcError; +pub use irc::error::IrcError; use plugin::*; @@ -72,7 +66,7 @@ pub struct Bot { impl Bot { /// Creates a `Bot`. - /// By itself the bot only responds to a few simple ctcp commands + /// 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). @@ -83,10 +77,12 @@ impl Bot { /// let mut bot = Bot::new(); /// ``` pub fn new() -> Bot { - Bot { plugins: ThreadedPlugins::new() } + Bot { + plugins: ThreadedPlugins::new(), + } } - /// Adds the plugin. + /// Adds the [`Plugin`](plugin/trait.Plugin.html). /// These plugins will be used to evaluate incoming messages from IRC. /// /// # Examples @@ -100,7 +96,7 @@ impl Bot { self.plugins.add(plugin); } - /// Removes a plugin based on its name. + /// Removes a [`Plugin`](plugin/trait.Plugin.html) based on its name. /// The binary currently uses this to disable plugins /// based on user configuration. /// @@ -116,66 +112,62 @@ impl Bot { self.plugins.remove(name) } - /// This connects the `Bot` to IRC and adds a task - /// to the Core that was supplied. + /// 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 core, so that frippy - /// can do its work. + /// 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 tokio_core; - /// # extern crate futures; + /// # extern crate irc; /// # extern crate frippy; /// # fn main() { /// use frippy::{Config, Bot}; - /// use tokio_core::reactor::Core; - /// use futures::future; + /// use irc::client::reactor::IrcReactor; /// /// let config = Config::load("config.toml").unwrap(); - /// let mut reactor = Core::new().unwrap(); + /// let mut reactor = IrcReactor::new().unwrap(); /// let mut bot = Bot::new(); /// - /// bot.connect(&mut reactor, &config); - /// reactor.run(future::empty::<(), ()>()).unwrap(); + /// bot.connect(&mut reactor, &config).unwrap(); + /// reactor.run().unwrap(); /// # } /// ``` - pub fn connect(&self, reactor: &mut Core, config: &Config) { + pub fn connect(&self, reactor: &mut IrcReactor, config: &Config) -> Result<(), String> { info!("Plugins loaded: {}", self.plugins); - let server = - match IrcServer::new_future(reactor.handle(), config).and_then(|f| {reactor.run(f)}) { - Ok(v) => v, - Err(e) => { - error!("Failed to connect to IRC server: {}", e); - return; - } - }; + let client = match reactor.prepare_client_and_connect(config) { + Ok(v) => v, + Err(e) => return Err(format!("Failed to connect: {}", e)), + }; info!("Connected to IRC server"); - match server.identify() { + match client.identify() { Ok(_) => info!("Identified"), - Err(e) => error!("Failed to identify: {}", e), + Err(e) => return Err(format!("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)); + reactor.register_client_with_handler(client, move |client, message| { + process_msg(client, plugins.clone(), message) + }); - reactor.handle().spawn(task); + Ok(()) } } -fn process_msg(server: &IrcServer, - mut plugins: ThreadedPlugins, - message: Message) - -> Result<(), IrcError> { - +fn process_msg( + server: &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() { @@ -205,7 +197,9 @@ struct ThreadedPlugins { impl ThreadedPlugins { pub fn new() -> ThreadedPlugins { - ThreadedPlugins { plugins: HashMap::new() } + ThreadedPlugins { + plugins: HashMap::new(), + } } pub fn add<T: Plugin + 'static>(&mut self, plugin: T) { @@ -219,37 +213,42 @@ impl ThreadedPlugins { self.plugins.remove(&name.to_lowercase()).map(|_| ()) } - pub fn execute_plugins(&mut self, server: &IrcServer, message: Message) { + pub fn execute_plugins(&mut self, server: &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 - 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); - }; - }); + match plugin.execute(server, &message) { + ExecutionStatus::Done => (), + ExecutionStatus::Err(e) => error!("Error in {} - {}", name, e), + ExecutionStatus::RequiresThread => { + debug!( + "Spawning thread to execute {} 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_threaded(&server, &message) { + error!("Error in {} - {}", name, e); + }; + }); + } } } } - pub fn handle_command(&mut self, - server: &IrcServer, - mut command: PluginCommand) - -> Result<(), IrcError> { - + pub fn handle_command( + &mut self, + server: &IrcClient, + 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); @@ -257,7 +256,6 @@ impl ThreadedPlugins { // 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); @@ -267,18 +265,19 @@ impl ThreadedPlugins { 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); - }; - }); + 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]); + let help = format!( + "\"{} {}\" is not a command, \ + try \"{0} help\" instead.", + server.current_nickname(), + command.tokens[0] + ); server.send_notice(&command.source, &help) } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cb4e384 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,166 @@ +#![cfg_attr(feature = "clippy", feature(plugin))] +#![cfg_attr(feature = "clippy", plugin(clippy))] + +extern crate frippy; +extern crate glob; +extern crate irc; +extern crate time; + +#[cfg(feature = "mysql")] +#[macro_use] +extern crate diesel_migrations; +#[cfg(feature = "mysql")] +extern crate diesel; + +#[macro_use] +extern crate log; + +use std::collections::HashMap; +use log::{Level, LevelFilter, Metadata, Record}; + +use irc::client::reactor::IrcReactor; +use glob::glob; + +use frippy::plugins; +use frippy::Config; + +#[cfg(feature = "mysql")] +embed_migrations!(); + +struct Logger; + +impl log::Log for Logger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.target().contains("frippy") + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + if record.metadata().level() >= Level::Debug { + println!( + "[{}]({}) {} -> {}", + time::now().rfc822(), + record.level(), + record.target(), + record.args() + ); + } else { + println!( + "[{}]({}) {}", + time::now().rfc822(), + record.level(), + record.args() + ); + } + } + } + + fn flush(&self) {} +} + +static LOGGER: Logger = Logger; + +fn main() { + log::set_max_level(if cfg!(debug_assertions) { + LevelFilter::Debug + } else { + LevelFilter::Info + }); + + log::set_logger(&LOGGER).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), + } + } + + // Without configs the bot would just idle + if configs.is_empty() { + error!("No config file found"); + return; + } + + // Create an event loop to run the connections on. + let mut reactor = IrcReactor::new().unwrap(); + + // Open a connection and add work for each config + for config in configs { + let mut disabled_plugins = None; + let mut mysql_url = None; + if let Some(ref options) = config.options { + if let Some(disabled) = options.get("disabled_plugins") { + disabled_plugins = Some(disabled + .split(",") + .map(|p| p.trim()) + .collect::<Vec<_>>()); + } + + mysql_url = options.get("mysql_url"); + } + + let mut bot = frippy::Bot::new(); + bot.add_plugin(plugins::Help::new()); + bot.add_plugin(plugins::Url::new(1024)); + bot.add_plugin(plugins::Emoji::new()); + bot.add_plugin(plugins::Currency::new()); + bot.add_plugin(plugins::KeepNick::new()); + bot.add_plugin(plugins::Tell::new()); + + #[cfg(feature = "mysql")] + { + if let Some(url) = mysql_url { + use diesel; + use diesel::Connection; + match diesel::mysql::MysqlConnection::establish(url) { + Ok(conn) => { + match embedded_migrations::run(&conn) { + Ok(_) => { + bot.add_plugin(plugins::Factoids::new(conn)); + info!("Connected to MySQL server") + } + Err(e) => { + bot.add_plugin(plugins::Factoids::new(HashMap::new())); + error!("Failed to run migrations: {}", e); + } + } + } + Err(e) => error!("Failed to connect to database: {}", e), + } + } else { + bot.add_plugin(plugins::Factoids::new(HashMap::new())); + } + } + #[cfg(not(feature = "mysql"))] + { + if let Some(_) = mysql_url { + error!("frippy was not built with the mysql feature") + } + bot.add_plugin(plugins::Factoids::new(HashMap::new())); + } + + + if let Some(disabled_plugins) = disabled_plugins { + for name in disabled_plugins { + if bot.remove_plugin(name).is_none() { + error!("\"{}\" was not found - could not disable", name); + } + } + } + + bot.connect(&mut reactor, &config) + .expect("Failed to connect"); + } + + // Run the bots until they throw an error - an error could be loss of connection + reactor.run().unwrap(); +} diff --git a/src/plugin.rs b/src/plugin.rs index d14c129..a67d68f 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -2,20 +2,35 @@ use std::fmt; use irc::client::prelude::*; -use irc::error::Error as IrcError; +use irc::error::IrcError; + +/// Describes if a [`Plugin`](trait.Plugin.html) is done working on a +/// [`Message`](../../irc/proto/message/struct.Message.html) or if another thread is required. +#[derive(Debug)] +pub enum ExecutionStatus { + /// The [`Plugin`](trait.Plugin.html) does not need to do any more work on this [`Message`](../../irc/proto/message/struct.Message.html). + Done, + /// An error occured during the execution. + Err(IrcError), + /// The execution needs to be done by [`execute_threaded()`](trait.Plugin.html#tymethod.execute_threaded). + RequiresThread, +} /// `Plugin` has to be implemented for any struct that should be usable -/// as a plugin in frippy. +/// 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 messages which are not commands or returns [`RequiresThread`](enum.ExecutionStatus.html#variant.RequiresThread) + /// if [`execute_threaded()`](trait.Plugin.html#tymethod.execute_threaded) should be used instead. + fn execute(&self, server: &IrcClient, message: &Message) -> ExecutionStatus; + /// Handles messages which are not commands in a new thread. + fn execute_threaded(&self, server: &IrcClient, message: &Message) -> Result<(), IrcError>; /// Handles any command directed at this plugin. - fn command(&self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError>; + fn command(&self, server: &IrcClient, command: PluginCommand) -> Result<(), IrcError>; + /// Similar to [`command()`](trait.Plugin.html#tymethod.command) but return a String instead of sending messages directly to IRC. + fn evaluate(&self, server: &IrcClient, command: PluginCommand) -> Result<String, String>; } -/// `PluginName` is required by `Plugin`. +/// `PluginName` is required by [`Plugin`](trait.Plugin.html). /// /// To implement it simply add `#[derive(PluginName)]` /// above the definition of the struct. @@ -28,7 +43,7 @@ pub trait Plugin: PluginName + Send + Sync + fmt::Debug { /// struct Foo; /// ``` pub trait PluginName: Send + Sync + fmt::Debug { - /// Returns the name of the plugin. + /// Returns the name of the `Plugin`. fn name(&self) -> &str; } @@ -45,28 +60,23 @@ pub struct PluginCommand { } impl PluginCommand { - /// Creates a `PluginCommand` from `Message` if it is a `PRIVMSG` + /// Creates a `PluginCommand` from [`Message`](../../irc/proto/message/struct.Message.html) + /// if it contains a [`PRIVMSG`](../../irc/proto/command/enum.Command.html#variant.PRIVMSG) /// that starts with the provided `nick`. 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(); + tokens[0] = tokens[0].chars().filter(|&c| !":,".contains(c)).collect(); if !tokens[0].is_empty() { return None; } @@ -75,10 +85,10 @@ impl PluginCommand { tokens.remove(0); Some(PluginCommand { - source: message.source_nickname().unwrap().to_string(), - target: message.response_target().unwrap().to_string(), - tokens: tokens, - }) + source: message.source_nickname().unwrap().to_string(), + target: message.response_target().unwrap().to_string(), + tokens: tokens, + }) } else { None } diff --git a/src/plugins/currency.rs b/src/plugins/currency.rs index 634faa2..958c8e2 100644 --- a/src/plugins/currency.rs +++ b/src/plugins/currency.rs @@ -6,7 +6,7 @@ use std::io::Read; use std::num::ParseFloatError; use irc::client::prelude::*; -use irc::error::Error as IrcError; +use irc::error::IrcError; use self::reqwest::Client; use self::reqwest::header::Connection; @@ -23,18 +23,8 @@ struct ConvertionRequest<'a> { target: &'a str, } -macro_rules! try_option { - ($e:expr) => { - match $e { - Some(v) => v, - None => { return None; } - } - } -} - impl<'a> ConvertionRequest<'a> { fn send(&self) -> Option<f64> { - let response = Client::new() .get("https://api.fixer.io/latest") .form(&[("base", self.source)]) @@ -44,16 +34,14 @@ impl<'a> ConvertionRequest<'a> { match response { Ok(mut response) => { let mut body = String::new(); - try_option!(response.read_to_string(&mut body).ok()); + response.read_to_string(&mut body).ok()?; let convertion_rates: Result<Value, _> = serde_json::from_str(&body); match convertion_rates { Ok(convertion_rates) => { - - let rates: &Value = try_option!(convertion_rates.get("rates")); - let target_rate: &Value = - try_option!(rates.get(self.target.to_uppercase())); - Some(self.value * try_option!(target_rate.as_f64())) + let rates: &Value = convertion_rates.get("rates")?; + let target_rate: &Value = rates.get(self.target.to_uppercase())?; + Some(self.value * target_rate.as_f64()?) } Err(_) => None, } @@ -68,7 +56,10 @@ impl Currency { Currency {} } - fn eval_command<'a>(&self, tokens: &'a [String]) -> Result<ConvertionRequest<'a>, ParseFloatError> { + fn eval_command<'a>( + &self, + tokens: &'a [String], + ) -> Result<ConvertionRequest<'a>, ParseFloatError> { Ok(ConvertionRequest { value: tokens[0].parse()?, source: &tokens[1], @@ -76,73 +67,89 @@ impl Currency { }) } - fn convert(&self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError> { - + fn convert(&self, client: &IrcClient, command: &mut PluginCommand) -> Result<String, String> { if command.tokens.len() < 3 { - return self.invalid_command(server, &command); + return Err(self.invalid_command(client)); } let request = match self.eval_command(&command.tokens) { Ok(request) => request, Err(_) => { - return self.invalid_command(server, &command); + return Err(self.invalid_command(client)); } }; match request.send() { Some(response) => { - let response = format!("{} {} => {:.4} {}", - request.value, - request.source.to_lowercase(), - response / 1.00000000, - request.target.to_lowercase()); - - server.send_privmsg(&command.target, &response) + let response = format!( + "{} {} => {:.4} {}", + request.value, + request.source.to_lowercase(), + response / 1.00000000, + request.target.to_lowercase() + ); + + Ok(response) } - None => server.send_notice(&command.source, "Error while converting given currency"), + None => Err(String::from( + "An error occured during the conversion of the given currency", + )), } } - 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, \ - CHF, CNY, CZK, DKK, GBP, HKD, HRK, HUF, \ - IDR, ILS, INR, JPY, KRW, MXN, MYR, NOK, \ - NZD, PHP, PLN, RON, RUB, SEK, SGD, THB, \ - TRY, USD, ZAR", - server.current_nickname()); - - server.send_notice(&command.source, &help) + fn help(&self, client: &IrcClient) -> String { + format!( + "usage: {} currency value from_currency to_currency\r\n\ + example: {0} currency 1.5 eur usd\r\n\ + available currencies: AUD, BGN, BRL, CAD, \ + CHF, CNY, CZK, DKK, GBP, HKD, HRK, HUF, \ + IDR, ILS, INR, JPY, KRW, MXN, MYR, NOK, \ + NZD, PHP, PLN, RON, RUB, SEK, SGD, THB, \ + TRY, USD, ZAR", + client.current_nickname() + ) } - fn invalid_command(&self, server: &IrcServer, command: &PluginCommand) -> Result<(), IrcError> { - let help = format!("Incorrect Command. \ - Send \"{} currency help\" for help.", - server.current_nickname()); - - server.send_notice(&command.source, &help) + fn invalid_command(&self, client: &IrcClient) -> String { + format!( + "Incorrect Command. \ + Send \"{} currency help\" for help.", + client.current_nickname() + ) } } impl Plugin for Currency { - fn is_allowed(&self, _: &IrcServer, _: &Message) -> bool { - false + fn execute(&self, _: &IrcClient, _: &Message) -> ExecutionStatus { + ExecutionStatus::Done } - fn execute(&self, _: &IrcServer, _: &Message) -> Result<(), IrcError> { + fn execute_threaded(&self, _: &IrcClient, _: &Message) -> Result<(), IrcError> { panic!("Currency does not implement the execute function!") } - fn command(&self, server: &IrcServer, mut command: PluginCommand) -> Result<(), IrcError> { + fn command(&self, client: &IrcClient, mut command: PluginCommand) -> Result<(), IrcError> { + if command.tokens.is_empty() { + return client.send_notice(&command.source, &self.invalid_command(client)); + } + + match command.tokens[0].as_ref() { + "help" => client.send_notice(&command.source, &self.help(client)), + _ => match self.convert(client, &mut command) { + Ok(msg) => client.send_privmsg(&command.target, &msg), + Err(msg) => client.send_notice(&command.source, &msg), + }, + } + } + fn evaluate(&self, client: &IrcClient, mut command: PluginCommand) -> Result<String, String> { if command.tokens.is_empty() { - return self.invalid_command(server, &command); + return Err(self.invalid_command(client)); } match command.tokens[0].as_ref() { - "help" => self.help(server, &mut command), - _ => self.convert(server, command), + "help" => Ok(self.help(client)), + _ => self.convert(client, &mut command), } } } diff --git a/src/plugins/emoji.rs b/src/plugins/emoji.rs index 59e2fdd..fcb04d1 100644 --- a/src/plugins/emoji.rs +++ b/src/plugins/emoji.rs @@ -3,7 +3,7 @@ extern crate unicode_names; use std::fmt; use irc::client::prelude::*; -use irc::error::Error as IrcError; +use irc::error::IrcError; use plugin::*; @@ -14,7 +14,6 @@ struct EmojiHandle { 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"), @@ -36,13 +35,12 @@ impl Emoji { Emoji {} } - fn emoji(&self, server: &IrcServer, content: &str, target: &str) -> Result<(), IrcError> { - let names = self.return_emojis(content) + fn emoji(&self, content: &str) -> String { + self.return_emojis(content) .iter() .map(|e| e.to_string()) - .collect::<Vec<String>>(); - - server.send_privmsg(target, &names.join(", ")) + .collect::<Vec<String>>() + .join(", ") } fn return_emojis(&self, string: &str) -> Vec<EmojiHandle> { @@ -53,7 +51,6 @@ impl Emoji { count: 0, }; - for c in string.chars() { if !self.is_emoji(&c) { continue; @@ -61,7 +58,6 @@ impl Emoji { if current.symbol == c { current.count += 1; - } else { if current.count > 0 { emojis.push(current); @@ -98,25 +94,36 @@ impl Emoji { } impl Plugin for Emoji { - fn is_allowed(&self, _: &IrcServer, message: &Message) -> bool { + fn execute(&self, client: &IrcClient, message: &Message) -> ExecutionStatus { match message.command { - Command::PRIVMSG(_, _) => true, - _ => false, + Command::PRIVMSG(_, ref content) => match client + .send_privmsg(message.response_target().unwrap(), &self.emoji(content)) + { + Ok(_) => ExecutionStatus::Done, + Err(e) => ExecutionStatus::Err(e), + }, + _ => ExecutionStatus::Done, } } - fn execute(&self, server: &IrcServer, message: &Message) -> Result<(), IrcError> { - match message.command { - Command::PRIVMSG(_, ref content) => { - self.emoji(server, content, message.response_target().unwrap()) - } - _ => Ok(()), - } + fn execute_threaded(&self, _: &IrcClient, _: &Message) -> Result<(), IrcError> { + panic!("Emoji should not use threading") + } + + fn command(&self, client: &IrcClient, command: PluginCommand) -> Result<(), IrcError> { + client.send_notice( + &command.source, + "This Plugin does not implement any commands.", + ) } - fn command(&self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError> { - server.send_notice(&command.source, - "This Plugin does not implement any commands.") + fn evaluate(&self, _: &IrcClient, command: PluginCommand) -> Result<String, String> { + let emojis = self.emoji(&command.tokens[0]); + if emojis.is_empty() { + Ok(emojis) + } else { + Err(String::from("No emojis were found.")) + } } } diff --git a/src/plugins/factoids/mod.rs b/src/plugins/factoids/mod.rs index 08e8f12..49ace10 100644 --- a/src/plugins/factoids/mod.rs +++ b/src/plugins/factoids/mod.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use std::sync::Mutex; use self::rlua::prelude::*; use irc::client::prelude::*; -use irc::error::Error as IrcError; +use irc::error::IrcError; use time; use chrono::NaiveDateTime; @@ -56,67 +56,67 @@ impl<T: Database> Factoids<T> { } } - fn add(&self, server: &IrcServer, command: &mut PluginCommand) -> Result<(), IrcError> { + fn add(&self, client: &IrcClient, command: &mut PluginCommand) -> Result<(), IrcError> { if command.tokens.len() < 2 { - return self.invalid_command(server, command); + return self.invalid_command(client, command); } let name = command.tokens.remove(0); let content = command.tokens.join(" "); match self.create_factoid(&name, &content, &command.source) { - Ok(v) => server.send_notice(&command.source, v), - Err(e) => server.send_notice(&command.source, e), + Ok(v) => client.send_notice(&command.source, v), + Err(e) => client.send_notice(&command.source, e), } } - fn from_url(&self, server: &IrcServer, command: &mut PluginCommand) -> Result<(), IrcError> { + fn from_url(&self, client: &IrcClient, command: &mut PluginCommand) -> Result<(), IrcError> { if command.tokens.len() < 2 { - return self.invalid_command(server, command); + return self.invalid_command(client, command); } let name = command.tokens.remove(0); let url = &command.tokens[0]; if let Some(content) = ::utils::download(1024, url) { match self.create_factoid(&name, &content, &command.source) { - Ok(v) => server.send_notice(&command.source, v), - Err(e) => server.send_notice(&command.source, e), + Ok(v) => client.send_notice(&command.source, v), + Err(e) => client.send_notice(&command.source, e), } } else { - server.send_notice(&command.source, "Failed to download.") + client.send_notice(&command.source, "Failed to download.") } } - fn remove(&self, server: &IrcServer, command: &mut PluginCommand) -> Result<(), IrcError> { + fn remove(&self, client: &IrcClient, command: &mut PluginCommand) -> Result<(), IrcError> { if command.tokens.len() < 1 { - return self.invalid_command(server, command); + return self.invalid_command(client, command); } let name = command.tokens.remove(0); let count = match try_lock!(self.factoids).count(&name) { Ok(c) => c, - Err(e) => return server.send_notice(&command.source, e), + Err(e) => return client.send_notice(&command.source, e), }; match try_lock!(self.factoids).delete(&name, count - 1) { - DbResponse::Success => server.send_notice(&command.source, "Successfully removed"), - DbResponse::Failed(e) => server.send_notice(&command.source, &e), + DbResponse::Success => client.send_notice(&command.source, "Successfully removed"), + DbResponse::Failed(e) => client.send_notice(&command.source, &e), } } - fn get(&self, server: &IrcServer, command: &PluginCommand) -> Result<(), IrcError> { + fn get(&self, client: &IrcClient, command: &PluginCommand) -> Result<(), IrcError> { let (name, idx) = match command.tokens.len() { - 0 => return self.invalid_command(server, command), + 0 => return self.invalid_command(client, command), 1 => { let name = &command.tokens[0]; let count = match try_lock!(self.factoids).count(name) { Ok(c) => c, - Err(e) => return server.send_notice(&command.source, e), + Err(e) => return client.send_notice(&command.source, e), }; if count < 1 { - return server.send_notice(&command.source, &format!("{} does not exist", name)); + return client.send_notice(&command.source, &format!("{} does not exist", name)); } (name, count - 1) @@ -125,7 +125,7 @@ impl<T: Database> Factoids<T> { let name = &command.tokens[0]; let idx = match i32::from_str(&command.tokens[1]) { Ok(i) => i, - Err(_) => return server.send_notice(&command.source, "Invalid index"), + Err(_) => return client.send_notice(&command.source, "Invalid index"), }; (name, idx) @@ -135,36 +135,36 @@ impl<T: Database> Factoids<T> { let factoid = match try_lock!(self.factoids).get(name, idx) { Some(v) => v, None => { - return server.send_notice(&command.source, + return client.send_notice(&command.source, &format!("{}~{} does not exist", name, idx)) } }; let message = factoid.content.replace("\n", "|").replace("\r", ""); - server.send_privmsg(&command.target, + client.send_privmsg(&command.target, &format!("{}: {}", factoid.name, message)) } - fn info(&self, server: &IrcServer, command: &PluginCommand) -> Result<(), IrcError> { + fn info(&self, client: &IrcClient, command: &PluginCommand) -> Result<(), IrcError> { match command.tokens.len() { - 0 => self.invalid_command(server, command), + 0 => self.invalid_command(client, command), 1 => { let name = &command.tokens[0]; let count = match try_lock!(self.factoids).count(name) { Ok(c) => c, - Err(e) => return server.send_notice(&command.source, e), + Err(e) => return client.send_notice(&command.source, e), }; match count { - 0 => server.send_notice(&command.source, &format!("{} does not exist", name)), + 0 => client.send_notice(&command.source, &format!("{} does not exist", name)), 1 => { - server.send_privmsg(&command.target, + client.send_privmsg(&command.target, &format!("There is 1 version of {}", name)) } _ => { - server.send_privmsg(&command.target, + client.send_privmsg(&command.target, &format!("There are {} versions of {}", count, name)) } } @@ -173,18 +173,18 @@ impl<T: Database> Factoids<T> { let name = &command.tokens[0]; let idx = match i32::from_str(&command.tokens[1]) { Ok(i) => i, - Err(_) => return server.send_notice(&command.source, "Invalid index"), + Err(_) => return client.send_notice(&command.source, "Invalid index"), }; let factoid = match try_lock!(self.factoids).get(name, idx) { Some(v) => v, None => { - return server.send_notice(&command.source, + return client.send_notice(&command.source, &format!("{}~{} does not exist", name, idx)) } }; - server.send_privmsg(&command.target, + client.send_privmsg(&command.target, &format!("{}: Added by {} at {} UTC", name, factoid.author, @@ -195,23 +195,23 @@ impl<T: Database> Factoids<T> { } fn exec(&self, - server: &IrcServer, + client: &IrcClient, mut command: PluginCommand, error: bool) -> Result<(), IrcError> { if command.tokens.len() < 1 { - self.invalid_command(server, &command) + self.invalid_command(client, &command) } else { let name = command.tokens.remove(0); let count = match try_lock!(self.factoids).count(&name) { Ok(c) => c, - Err(e) => return server.send_notice(&command.source, e), + Err(e) => return client.send_notice(&command.source, e), }; let factoid = match try_lock!(self.factoids).get(&name, count - 1) { Some(v) => v.content, - None if error => return self.invalid_command(server, &command), + None if error => return self.invalid_command(client, &command), None => return Ok(()), }; @@ -230,7 +230,7 @@ impl<T: Database> Factoids<T> { factoid }; - server.send_privmsg(&command.target, &value.replace("\n", "|").replace("\r", "")) + client.send_privmsg(&command.target, &value.replace("\n", "|").replace("\r", "")) } } @@ -265,20 +265,24 @@ impl<T: Database> Factoids<T> { Ok(output.join("|")) } - fn invalid_command(&self, server: &IrcServer, command: &PluginCommand) -> Result<(), IrcError> { - server.send_notice(&command.source, "Invalid Command") + fn invalid_command(&self, client: &IrcClient, command: &PluginCommand) -> Result<(), IrcError> { + client.send_notice(&command.source, "Invalid Command") } } impl<T: Database> Plugin for Factoids<T> { - fn is_allowed(&self, _: &IrcServer, message: &Message) -> bool { + fn execute(&self, _: &IrcClient, message: &Message) -> ExecutionStatus { match message.command { - Command::PRIVMSG(_, ref content) => content.starts_with('!'), - _ => false, + Command::PRIVMSG(_, ref content) => if content.starts_with('!') { + ExecutionStatus::RequiresThread + } else { + ExecutionStatus::Done + }, + _ => ExecutionStatus::Done, } } - fn execute(&self, server: &IrcServer, message: &Message) -> Result<(), IrcError> { + fn execute_threaded(&self, client: &IrcClient, message: &Message) -> Result<(), IrcError> { if let Command::PRIVMSG(_, mut content) = message.command.clone() { content.remove(0); @@ -290,29 +294,33 @@ impl<T: Database> Plugin for Factoids<T> { tokens: t, }; - self.exec(server, c, false) + self.exec(client, c, false) } else { Ok(()) } } - fn command(&self, server: &IrcServer, mut command: PluginCommand) -> Result<(), IrcError> { + fn command(&self, client: &IrcClient, mut command: PluginCommand) -> Result<(), IrcError> { if command.tokens.is_empty() { - return self.invalid_command(server, &command); + return self.invalid_command(client, &command); } let sub_command = command.tokens.remove(0); match sub_command.as_ref() { - "add" => self.add(server, &mut command), - "fromurl" => self.from_url(server, &mut command), - "remove" => self.remove(server, &mut command), - "get" => self.get(server, &command), - "info" => self.info(server, &command), - "exec" => self.exec(server, command, true), - _ => self.invalid_command(server, &command), + "add" => self.add(client, &mut command), + "fromurl" => self.from_url(client, &mut command), + "remove" => self.remove(client, &mut command), + "get" => self.get(client, &command), + "info" => self.info(client, &command), + "exec" => self.exec(client, command, true), + _ => self.invalid_command(client, &command), } } + + fn evaluate(&self, _: &IrcClient, _: PluginCommand) -> Result<String, String> { + Err(String::from("Evaluation of commands is not implemented for Factoids at this time")) + } } impl<T: Database> fmt::Debug for Factoids<T> { diff --git a/src/plugins/help.rs b/src/plugins/help.rs index 7b987d4..4dd93d7 100644 --- a/src/plugins/help.rs +++ b/src/plugins/help.rs @@ -1,5 +1,5 @@ use irc::client::prelude::*; -use irc::error::Error as IrcError; +use irc::error::IrcError; use plugin::*; @@ -10,23 +10,23 @@ 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(&self, _: &IrcClient, _: &Message) -> ExecutionStatus { + ExecutionStatus::Done + } + + fn execute_threaded(&self, _: &IrcClient, _: &Message) -> Result<(), IrcError> { + panic!("Help should not use threading") } - fn execute(&self, _: &IrcServer, _: &Message) -> Result<(), IrcError> { - panic!("Help does not implement the execute function!") + fn command(&self, client: &IrcClient, command: PluginCommand) -> Result<(), IrcError> { + client.send_notice(&command.source, "Help has not been added yet.") } - fn command(&self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError> { - self.help(server, command) + fn evaluate(&self, _: &IrcClient, _: PluginCommand) -> Result<String, String> { + Err(String::from("Help has not been added yet.")) } } diff --git a/src/plugins/keepnick.rs b/src/plugins/keepnick.rs index 1d4627d..73f4893 100644 --- a/src/plugins/keepnick.rs +++ b/src/plugins/keepnick.rs @@ -1,5 +1,5 @@ use irc::client::prelude::*; -use irc::error::Error as IrcError; +use irc::error::IrcError; use plugin::*; @@ -11,48 +11,53 @@ impl KeepNick { KeepNick {} } - fn check_nick(&self, server: &IrcServer, leaver: &str) -> Result<(), IrcError> { - let cfg_nick = match server.config().nickname { + fn check_nick(&self, client: &IrcClient, leaver: &str) -> ExecutionStatus { + let cfg_nick = match client.config().nickname { Some(ref nick) => nick.clone(), - None => return Ok(()), + None => return ExecutionStatus::Done, }; if leaver != cfg_nick { - return Ok(()); + return ExecutionStatus::Done; } - let server_nick = server.current_nickname(); - - if server_nick != cfg_nick { - info!("Trying to switch nick from {} to {}", server_nick, cfg_nick); - server.send(Command::NICK(cfg_nick)) + let client_nick = client.current_nickname(); + if client_nick != cfg_nick { + info!("Trying to switch nick from {} to {}", client_nick, cfg_nick); + match client.send(Command::NICK(cfg_nick)) { + Ok(_) => ExecutionStatus::Done, + Err(e) => ExecutionStatus::Err(e), + } } else { - Ok(()) + ExecutionStatus::Done } } } impl Plugin for KeepNick { - fn is_allowed(&self, _: &IrcServer, message: &Message) -> bool { - match message.command { - Command::QUIT(_) => true, - _ => false, - } - } - - fn execute(&self, server: &IrcServer, message: &Message) -> Result<(), IrcError> { + fn execute(&self, client: &IrcClient, message: &Message) -> ExecutionStatus { match message.command { Command::QUIT(ref nick) => { - self.check_nick(server, &nick.clone().unwrap_or_else(|| String::new())) + self.check_nick(client, &nick.clone().unwrap_or_else(String::new)) } - _ => Ok(()), + _ => ExecutionStatus::Done, } } - fn command(&self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError> { - server.send_notice(&command.source, - "This Plugin does not implement any commands.") + fn execute_threaded(&self, _: &IrcClient, _: &Message) -> Result<(), IrcError> { + panic!("Tell should not use threading") + } + + fn command(&self, client: &IrcClient, command: PluginCommand) -> Result<(), IrcError> { + client.send_notice( + &command.source, + "This Plugin does not implement any commands.", + ) + } + + fn evaluate(&self, _: &IrcClient, _: PluginCommand) -> Result<String, String> { + Err(String::from("This Plugin does not implement any commands.")) } } diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 2e85932..5b32efd 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -2,6 +2,7 @@ mod help; mod url; mod emoji; +mod tell; mod currency; mod factoids; mod keepnick; @@ -9,6 +10,7 @@ mod keepnick; pub use self::help::Help; pub use self::url::Url; pub use self::emoji::Emoji; +pub use self::tell::Tell; pub use self::currency::Currency; pub use self::factoids::Factoids; pub use self::factoids::database; diff --git a/src/plugins/tell.rs b/src/plugins/tell.rs new file mode 100644 index 0000000..34d7cf8 --- /dev/null +++ b/src/plugins/tell.rs @@ -0,0 +1,140 @@ +use irc::client::prelude::*; +use irc::error::IrcError; + +use std::collections::HashMap; +use std::sync::Mutex; + +use plugin::*; + +macro_rules! try_lock { + ( $m:expr ) => { + match $m.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } + } +} + +#[derive(PluginName, Default, Debug)] +pub struct Tell { + tells: Mutex<HashMap<String, Vec<TellMessage>>>, +} + +#[derive(Default, Debug)] +struct TellMessage { + sender: String, + // TODO Add time + message: String, +} + +impl Tell { + pub fn new() -> Tell { + Tell { + tells: Mutex::new(HashMap::new()), + } + } + + fn tell_command(&self, client: &IrcClient, command: &PluginCommand) -> Result<&str, String> { + if command.tokens.len() < 2 { + return Err(self.invalid_command(client)); + } + + let receiver = command.tokens[0].to_string(); + let sender = command.source.to_owned(); + + if receiver == sender { + return Err(String::from("That's your name!")); + } + + if command.source != command.target { + if let Some(users) = client.list_users(&command.target) { + if users.iter().any(|u| u.get_nickname() == receiver) { + return Err(format!("{} is in this channel.", receiver)); + } + } + } + + let message = command.tokens[1..].join(" "); + let tell = TellMessage { + sender: sender, + message: message, + }; + + let mut tells = try_lock!(self.tells); + let tell_messages = tells.entry(receiver).or_insert(Vec::with_capacity(3)); + (*tell_messages).push(tell); + + Ok("Got it!") + } + + fn send_tell(&self, client: &IrcClient, receiver: &str) -> ExecutionStatus { + let mut tells = try_lock!(self.tells); + if let Some(tell_messages) = tells.get_mut(receiver) { + for tell in tell_messages { + if let Err(e) = client.send_notice( + receiver, + &format!("Tell from {}: {}", tell.sender, tell.message), + ) { + return ExecutionStatus::Err(e); + } + debug!( + "Sent {:?} from {:?} to {:?}", + tell.message, tell.sender, receiver + ); + } + } + tells.remove(receiver); + ExecutionStatus::Done + } + + fn invalid_command(&self, client: &IrcClient) -> String { + format!( + "Incorrect Command. \ + Send \"{} tell help\" for help.", + client.current_nickname() + ) + } + + fn help(&self, client: &IrcClient) -> String { + format!( + "usage: {} tell user message\r\n\ + example: {0} tell Foobar Hello!", + client.current_nickname() + ) + } +} + +impl Plugin for Tell { + fn execute(&self, client: &IrcClient, message: &Message) -> ExecutionStatus { + match message.command { + Command::JOIN(_, _, _) => self.send_tell(client, message.source_nickname().unwrap()), + Command::PRIVMSG(_, _) => self.send_tell(client, message.source_nickname().unwrap()), + _ => ExecutionStatus::Done, + } + } + + fn execute_threaded(&self, _: &IrcClient, _: &Message) -> Result<(), IrcError> { + panic!("Tell should not use threading") + } + + fn command(&self, client: &IrcClient, command: PluginCommand) -> Result<(), IrcError> { + if command.tokens.is_empty() { + return client.send_notice(&command.source, &self.invalid_command(client)); + } + + match command.tokens[0].as_ref() { + "help" => client.send_notice(&command.source, &self.help(client)), + _ => match self.tell_command(client, &command) { + Ok(msg) => client.send_notice(&command.source, msg), + Err(msg) => client.send_notice(&command.source, &msg), + }, + } + } + + fn evaluate(&self, _: &IrcClient, _: PluginCommand) -> Result<String, String> { + Err(String::from("This Plugin does not implement any commands.")) + } +} + +#[cfg(test)] +mod tests {} diff --git a/src/plugins/url.rs b/src/plugins/url.rs index 6f4a68f..df4fdf2 100644 --- a/src/plugins/url.rs +++ b/src/plugins/url.rs @@ -2,7 +2,7 @@ extern crate regex; extern crate select; use irc::client::prelude::*; -use irc::error::Error as IrcError; +use irc::error::IrcError; use self::regex::Regex; @@ -24,7 +24,7 @@ pub struct Url { impl Url { /// If a file is larger than `max_kib` KiB the download is stopped pub fn new(max_kib: usize) -> Url { - Url {max_kib: max_kib} + Url { max_kib: max_kib } } fn grep_url(&self, msg: &str) -> Option<String> { @@ -38,56 +38,63 @@ impl Url { } } - fn url(&self, server: &IrcServer, message: &str, target: &str) -> Result<(), IrcError> { - let url = match self.grep_url(message) { + fn url(&self, text: &str) -> Result<String, &str> { + let url = match self.grep_url(text) { Some(url) => url, - None => { - return Ok(()); - } + None => return Err("No Url was found."), }; match utils::download(self.max_kib, &url) { Some(body) => { - let doc = Document::from(body.as_ref()); if let Some(title) = doc.find(Name("title")).next() { - let text = title.children().next().unwrap(); - let message = text.as_text().unwrap().trim().replace("\n", "|"); - debug!("Title: {:?}", text); - debug!("Message: {:?}", message); - - server.send_privmsg(target, &message) + let title = title.children().next().unwrap(); + let title_text = title.as_text().unwrap().trim().replace("\n", "|"); + debug!("Title: {:?}", title); + debug!("Text: {:?}", title_text); + Ok(title_text) } else { - Ok(()) + Err("No title was found.") } } - None => Ok(()), + None => Err("Failed to download document."), } } } impl Plugin for Url { - fn is_allowed(&self, _: &IrcServer, message: &Message) -> bool { + fn execute(&self, _: &IrcClient, message: &Message) -> ExecutionStatus { match message.command { - Command::PRIVMSG(_, ref msg) => RE.is_match(msg), - _ => false, + Command::PRIVMSG(_, ref msg) => if RE.is_match(msg) { + ExecutionStatus::RequiresThread + } else { + ExecutionStatus::Done + }, + _ => ExecutionStatus::Done, } } - fn execute(&self, server: &IrcServer, message: &Message) -> Result<(), IrcError> { + fn execute_threaded(&self, client: &IrcClient, message: &Message) -> Result<(), IrcError> { match message.command { - Command::PRIVMSG(_, ref content) => { - self.url(server, content, message.response_target().unwrap()) - } + Command::PRIVMSG(_, ref content) => match self.url(content) { + Ok(title) => client.send_privmsg(message.response_target().unwrap(), &title), + Err(_) => Ok(()), + }, _ => Ok(()), } } - fn command(&self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError> { - server.send_notice(&command.source, - "This Plugin does not implement any commands.") + fn command(&self, client: &IrcClient, command: PluginCommand) -> Result<(), IrcError> { + client.send_notice( + &command.source, + "This Plugin does not implement any commands.", + ) + } + + fn evaluate(&self, _: &IrcClient, command: PluginCommand) -> Result<String, String> { + self.url(&command.tokens[0]).map_err(String::from) } } |
