summaryrefslogtreecommitdiffstats
path: root/src/lib.rs
blob: c556a328cf2824d53f6460b463f8cd50872160b1 (plain) (blame)
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
#![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
//! ```no_run
//! extern crate frippy;
//!
//! frippy::run();
//! ```

#[macro_use]
extern crate plugin_derive;

extern crate irc;
extern crate regex;

mod plugin;
mod plugins;

use std::thread::spawn;
use std::sync::{Arc, Mutex};
use irc::client::prelude::*;
use irc::proto::Command::PRIVMSG;
use irc::error::Error as IrcError;

use plugin::*;

// 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(),
        }
    }
}

/// Runs the bot
///
/// # Remarks
///
/// 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();
                }
            }

            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();

                    // Execute the plugin in another thread
                    spawn(move || { lock_plugin!(plugin).execute(&server, &message).unwrap(); });
                }

                // Check if the command is for this plugin
                // Clone it for the move
                if let Some(mut c) = command.clone() {

                    // Skip empty commands
                    if c.tokens.is_empty() { continue; }

                    if lock_plugin!(plugin).name().to_lowercase() == c.tokens[0].to_lowercase() {

                        // The first token contains the name of the plugin
                        c.tokens.remove(0);

                        // 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();
}

fn send_help_message(server: &IrcServer, command: &PluginCommand) -> Result<(), IrcError> {
    server.send_notice(&command.source, "Help has not been added yet.")
}

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(' ')
            .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
    }
}

#[cfg(test)]
mod tests {}