summaryrefslogtreecommitdiffstats
path: root/src/plugin.rs
blob: e0a4ce216c282cab95c6e5d847ff2ed720c7fdf2 (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
use std::fmt;
use std::collections::HashMap;
use std::thread::spawn;
use std::sync::{Arc, Mutex};

use irc::client::prelude::*;
use irc::error::Error as IrcError;

pub trait Plugin: PluginName + Send + Sync + fmt::Debug {
    fn is_allowed(&self, server: &IrcServer, message: &Message) -> bool;
    fn execute(&mut self, server: &IrcServer, message: &Message) -> Result<(), IrcError>;
    fn command(&mut self, server: &IrcServer, command: PluginCommand) -> Result<(), IrcError>;
}

pub trait PluginName: Send + Sync + fmt::Debug {
    fn name(&self) -> &str;
}

#[derive(Clone, Debug)]
pub struct PluginCommand {
    pub source: String,
    pub target: String,
    pub tokens: Vec<String>,
}

impl PluginCommand {
    pub fn from(nick: &str, message: &Message) -> Option<PluginCommand> {

        // Get the actual message out of PRIVMSG
        if let Command::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
        }
    }
}

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

#[derive(Clone, Debug)]
pub struct ThreadedPlugins {
    plugins: HashMap<String, Arc<Mutex<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(Mutex::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 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 || {
                          if let Err(e) = lock_plugin!(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);

            // 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) = lock_plugin!(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)| lock_plugin!(p).name().to_string())
            .collect::<Vec<String>>();
        write!(f, "{}", plugin_names.join(", "))
    }
}