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
//! # matrix_bot_api
//! Easy to use API for implementing your own Matrix-Bot (see matrix.org)
//!
//! # Basic setup:
//! There are two parts: A MessageHandler and the MatrixBot.
//! The MessageHandler defines what happens with received messages.
//! The MatrixBot consumes your MessageHandler and deals with all
//! the matrix-protocol-stuff, calling your MessageHandler for each
//! new text-message.
//!
//! You can write your own MessageHandler by implementing the `MessageHandler`-trait,
//! or use one provided by this crate (currently only `StatelessHandler`).
//!
//! # Multple Handlers:
//! One can register multiple MessageHandlers with a bot. Thus one can "plug and play"
//! different features to ones MatrixBot.
//! Messages are given to each handler in the order of their registration.
//! A message is given to the next handler until one handler returns `StopHandling`.
//! Thus a message can be handled by multiple handlers as well (for example for "help").
//!
//! # Example
//! ```
//! extern crate matrix_bot_api;
//! use matrix_bot_api::{MatrixBot, MessageType};
//! use matrix_bot_api::handlers::{StatelessHandler, HandleResult};
//!
//! fn main() {
//!     let mut handler = StatelessHandler::new();
//!     handler.register_handle("shutdown", |bot: &MatrixBot, _room: &str, _cmd: &str| {
//!         bot.shutdown();
//!         HandleResult::ContinueHandling /* Other handlers might need to clean up after themselves on shutdown */
//!     });
//!
//!     handler.register_handle("echo", |bot: &MatrixBot, room: &str, cmd: &str| {
//!         bot.send_message(&format!("Echo: {}", cmd), room, MessageType::TextMessage);
//!         HandleResult::StopHandling
//!     });
//!
//!     let mut bot = MatrixBot::new(handler);
//!     bot.run("your_bot", "secret_password", "https://your.homeserver");
//! }
//! ```
//! Have a look in the examples/ directory for detailed examples.

extern crate fractal_matrix_api;
extern crate chrono;
use self::chrono::prelude::*;

use fractal_matrix_api::backend::Backend;
use fractal_matrix_api::backend::BKCommand;
use fractal_matrix_api::backend::BKResponse;
use fractal_matrix_api::types::{Room, Message};

use std::sync::mpsc::channel;
use std::sync::mpsc::{Sender, Receiver};

pub mod handlers;
use handlers::{MessageHandler, HandleResult};

/// How messages from the bot should be formated. This is up to the client,
/// but usually RoomNotice's have a different color than TextMessage's.
pub enum MessageType {
    RoomNotice,
    TextMessage,
}


pub struct MatrixBot {
    backend: Sender<BKCommand>,
    rx: Receiver<BKResponse>,
    uid: Option<String>,
    verbose: bool,
    handlers: Option<Vec<Box<MessageHandler>>>,
}

impl MatrixBot {
    /// Consumes any struct that implements the MessageHandler-trait.
    pub fn new<M>(handler: M) -> MatrixBot
        where M: handlers::MessageHandler + 'static {
        let (tx, rx): (Sender<BKResponse>, Receiver<BKResponse>) = channel();
        let bk = Backend::new(tx);
        // Here it would be ideal to extend fractal_matrix_api in order to be able to give
        // sync a limit-parameter.
        // Until then, the workaround is to send "since" of the backend to "now".
        // Not interested in any messages since login
        bk.data.lock().unwrap().since = Local::now().to_string();
        MatrixBot {
            backend: bk.run(),
            rx: rx,
            uid: None,
            verbose: false,
            handlers: Some(vec![Box::new(handler)])
        }
    }

    /// Add an additional handler.
    /// Each message will be given to all registered handlers until
    /// one of them returns "HandleResult::StopHandling".
    pub fn add_handler<M>(&mut self, handler: M)
    where M: handlers::MessageHandler + 'static {
        if let Some(mut handlers) = self.handlers.take() {
            handlers.push(Box::new(handler));
            self.handlers = Some(handlers)
        }
    }

    /// If true, will print all Matrix-message coming in and going out (quite verbose!) to stdout
    /// Default: false
    pub fn set_verbose(&mut self, verbose: bool) {
        self.verbose = verbose;
    }

    /// Blocking call that runs as long as the Bot is running.
    /// Will call for each incoming text-message the given MessageHandler.
    /// Bot will automatically join all rooms it is invited to.
    /// Will return on shutdown only.
    /// All messages prior to run() will be ignored.
    pub fn run(mut self, user: &str, password: &str, homeserver_url: &str) {
        self.backend
            .send(BKCommand::Login(
                user.to_string(),
                password.to_string(),
                homeserver_url.to_string(),
            ))
            .unwrap();
        loop {
            let cmd = self.rx.recv().unwrap();
            if !self.handle_recvs(cmd) {
                break;
            }
        }
    }

    /// Will shutdown the bot. The bot will not leave any rooms.
    pub fn shutdown(&self) {
        self.backend.send(BKCommand::ShutDown).unwrap();
    }

    /// Will leave the given room (give room-id, not room-name)
    pub fn leave_room(&self, room_id: &str) {
        self.backend
            .send(BKCommand::LeaveRoom(room_id.to_string()))
            .unwrap();
    }

    /// Sends a message to a given room, with a given message-type.
    ///  * msg:     The incoming message
    ///  * room:    The room-id, the message should be sent to
    ///  * msgtype: Type of message (text or notice)
    pub fn send_message(&self, msg: &str, room: &str, msgtype: MessageType) {
        let uid = self.uid.clone().unwrap_or_default();
        let mtype = match msgtype {
            MessageType::RoomNotice => "m.notice".to_string(),
            MessageType::TextMessage => "m.text".to_string(),
        };

        let mut m = Message {
            sender: uid,
            mtype: mtype,
            body: msg.to_string(),
            room: room.to_string(),
            date: Local::now(),
            thumb: None,
            url: None,
            id: None,
            formatted_body: None,
            format: None,
        };
        m.id = Some(m.get_txn_id());

        if self.verbose {
            println!("===> sending: {:?}", m);
        }
        self.backend.send(BKCommand::SendMsg(m)).unwrap();
    }

    /* --------- Private functions ------------ */
    fn handle_recvs(&mut self, resp: BKResponse) -> bool {
        if self.verbose {
            println!("<=== received: {:?}", resp);
        }

        match resp {
            BKResponse::NewRooms(x) => self.handle_rooms(x),
            //BKResponse::Rooms(x, _) => self.handle_rooms(x),
            BKResponse::RoomMessages(x) => self.handle_messages(x),
            BKResponse::Token(uid, _) => {
                self.uid = Some(uid); // Successfull login
                self.backend.send(BKCommand::Sync).unwrap();
            }
            BKResponse::Sync(_) => self.backend.send(BKCommand::Sync).unwrap(),
            BKResponse::SyncError(_) => self.backend.send(BKCommand::Sync).unwrap(),
            BKResponse::ShutDown => {
                return false;
            }
            _ => (),
        }
        true
    }

    fn handle_messages(&mut self, messages: Vec<Message>) {

        for message in messages {
            /* First of all, mark all new messages as "read" */
            self.backend
                .send(BKCommand::MarkAsRead(
                    message.room.clone(),
                    message.id.clone().unwrap_or_default(),
                ))
                .unwrap();

            // It might be a command for us, if the message is text
            // and if its not from the bot itself
            let uid = self.uid.clone().unwrap_or_default();
            // This might be a command for us (only text-messages are interesting)
            if message.mtype == "m.text" && message.sender != uid {
                // We take the handlers, in order to be able to borrow self (MatrixBot)
                // and hand it to the handler-function. After successfull call, we
                // reset the handlers.
                if let Some(mut handlers) = self.handlers.take() {
                    for mut handler in &mut handlers {
                        match handler.handle_message(&self, &message.room, &message.body) {
                            HandleResult::ContinueHandling => continue,
                            HandleResult::StopHandling     => break,
                        }
                    }
                    self.handlers = Some(handlers);
                }
            }
        }
    }

    fn handle_rooms(&self, rooms: Vec<Room>) {
        for rr in rooms {
            if rr.inv {
                self.backend
                    .send(BKCommand::JoinRoom(rr.id.clone()))
                    .unwrap();
                println!("Joining room {}", rr.id.clone());
            }
        }
    }
}