1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
|
#![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 irc;
//! # extern crate frippy;
//! # fn main() {
//! use frippy::{plugins, 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.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 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 lazy_static;
#[macro_use]
extern crate log;
extern crate antidote;
extern crate chrono;
extern crate circular_queue;
extern crate humantime;
extern crate irc;
extern crate regex;
extern crate reqwest;
extern crate time;
pub mod error;
pub mod plugin;
pub mod plugins;
pub mod utils;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use std::thread;
use error::*;
use failure::ResultExt;
pub use irc::client::prelude::*;
pub use irc::error::IrcError;
use plugin::*;
/// The bot which contains the main logic.
#[derive(Default)]
pub struct Bot<'a> {
prefix: &'a str,
plugins: ThreadedPlugins,
}
impl<'a> Bot<'a> {
/// Creates a `Bot` without any plugins.
/// 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).
/// To send commands to a plugin
/// the message has to start with the plugin's name prefixed by `cmd_prefix`.
///
/// # Examples
/// ```
/// use frippy::Bot;
/// let mut bot = Bot::new(".");
/// ```
pub fn new(cmd_prefix: &'a str) -> Bot<'a> {
Bot {
prefix: cmd_prefix,
plugins: ThreadedPlugins::new(),
}
}
/// 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);
}
/// 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)
}
/// 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);
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 twice
let plugins = self.plugins.clone();
let prefix = self.prefix.to_owned();
reactor.register_client_with_handler(client, move |client, message| {
process_msg(client, plugins.clone(), &prefix.clone(), message)
});
Ok(())
}
}
fn process_msg(
client: &IrcClient,
mut plugins: ThreadedPlugins,
prefix: &str,
message: Message,
) -> Result<(), IrcError> {
// Log any channels we join
if let Command::JOIN(ref channel, _, _) = message.command {
if message.source_nickname().unwrap() == client.current_nickname() {
info!("Joined {}", channel);
}
}
// Check for possible command and save the result for later
let command = PluginCommand::try_from(prefix, &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(client, command) {
error!("Failed to handle command: {}", e);
}
}
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);
self.plugins.insert(name, safe_plugin);
}
pub fn remove(&mut self, name: &str) -> Option<()> {
self.plugins.remove(&name.to_lowercase()).map(|_| ())
}
/// Runs the execute functions on all plugins.
/// Any errors that occur are printed right away.
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
if let Err(e) = thread::Builder::new()
.name(name)
.spawn(move || {
if let Err(e) = plugin.execute_threaded(&client, &message) {
log_error(&e);
} else {
debug!("{} sent response from thread", plugin.name());
}
})
.context(ErrorKind::ThreadSpawn)
{
log_error(&e.into());
}
}
}
}
}
pub fn handle_command(
&mut self,
client: &IrcClient,
mut command: PluginCommand,
) -> Result<(), FrippyError> {
// Check if there is a plugin for this command
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);
thread::Builder::new()
.name(name)
.spawn(move || {
if let Err(e) = plugin.command(&client, command) {
log_error(&e);
};
})
.context(ErrorKind::ThreadSpawn)?;
}
Ok(())
}
}
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(", "))
}
}
|