aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib.rs
diff options
context:
space:
mode:
authorJokler <jokler.contact@gmail.com>2018-03-12 16:02:51 +0100
committerJokler <jokler.contact@gmail.com>2018-03-12 16:02:51 +0100
commit909cabe9280722e43c5fb283f768051bb85e1890 (patch)
tree506ac34b7e22cdb95568cef9e649ee64cb3b0fdb /src/lib.rs
parent15e855ddecfdac31ddda26b12fcfd1a142a0ec21 (diff)
parent8e40e919aca8b8592be43e2c5bbcc0717bf14a6b (diff)
downloadfrippy-909cabe9280722e43c5fb283f768051bb85e1890.tar.gz
frippy-909cabe9280722e43c5fb283f768051bb85e1890.zip
Merge branch 'dev'
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs339
1 files changed, 252 insertions, 87 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 324e273..ebadb86 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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(", "))
+ }
+}