summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs210
-rw-r--r--src/plugin.rs140
-rw-r--r--src/plugins/currency.rs58
-rw-r--r--src/plugins/emoji.rs74
-rw-r--r--src/plugins/help.rs34
-rw-r--r--src/plugins/mod.rs9
6 files changed, 352 insertions, 173 deletions
diff --git a/src/lib.rs b/src/lib.rs
index e1ca7e3..324e273 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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;