aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib.rs
diff options
context:
space:
mode:
authorJokler <jokler.contact@gmail.com>2017-11-28 03:12:48 +0100
committerJokler <jokler.contact@gmail.com>2017-11-28 03:25:45 +0100
commit09113bf4fa8cb8a42adb72533c3c76279e090978 (patch)
treecfb65ad986dac30cf80f88ac36165dba6eab322d /src/lib.rs
parent15e855ddecfdac31ddda26b12fcfd1a142a0ec21 (diff)
downloadfrippy-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.rs269
1 files changed, 205 insertions, 64 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 {}