summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs269
-rw-r--r--src/plugin.rs120
2 files changed, 229 insertions, 160 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 324e273..9613160 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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(", "))
- }
-}