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
//! # matrix_bot_api //! Easy to use API for implementing your own Matrix-Bot (see matrix.org) //! //! # Basic setup: //! There are two main 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 with an [`ActiveBot`] handle that allows the handler to //! respond to the 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, _, _| { //! bot.shutdown(); //! HandleResult::ContinueHandling /* Other handlers might need to clean up after themselves on shutdown */ //! }); //! //! handler.register_handle("echo", |bot, message, tail| { //! bot.send_message(&format!("Echo: {}", tail), &message.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. //! //! [`MatrixBot`]: struct.MatrixBot.html //! [`ActiveBot`]: struct.ActiveBot.html //! [`MessageHandler`]: handlers/trait.MessageHandler.html //! [`StatelessHandler`]: handlers/stateless_handler/struct.StatelessHandler.html use chrono::prelude::*; use fractal_matrix_api::backend::BKCommand; use fractal_matrix_api::backend::BKResponse; use fractal_matrix_api::backend::Backend; use fractal_matrix_api::types::message::get_txn_id; pub use fractal_matrix_api::types::{Message, Room}; use std::sync::mpsc::channel; use std::sync::mpsc::{Receiver, Sender}; pub mod handlers; use handlers::{HandleResult, MessageHandler}; /// How messages from the bot should be formatted. 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: Vec<Box<dyn MessageHandler + Send>>, } impl MatrixBot { /// Consumes any struct that implements the MessageHandler-trait. pub fn new<M>(handler: M) -> MatrixBot where M: handlers::MessageHandler + 'static + Send, { 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 = Some(Local::now().to_string()); MatrixBot { backend: bk.run(), rx, uid: None, verbose: false, handlers: vec![Box::new(handler)], } } /// Create a copy of the internal ActiveBot instance for sending messages pub fn get_activebot_clone(&self) -> ActiveBot { ActiveBot { backend: self.backend.clone(), uid: self.uid.clone(), verbose: self.verbose, } } /// 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 + Send, { self.handlers.push(Box::new(handler)); } /// 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(); let mut active_bot = self.get_activebot_clone(); for handler in self.handlers.iter_mut() { handler.init_handler(&active_bot); } loop { let cmd = self.rx.recv().unwrap(); if !self.handle_recvs(cmd, &mut active_bot) { break; } } } /* --------- Private functions ------------ */ fn handle_recvs(&mut self, resp: BKResponse, active_bot: &mut ActiveBot) -> bool { if self.verbose { println!("<=== received: {:?}", resp); } match resp { BKResponse::UpdateRooms(x) => self.handle_rooms(x), //BKResponse::Rooms(x, _) => self.handle_rooms(x), BKResponse::RoomMessages(x) => self.handle_messages(x, active_bot), BKResponse::Token(uid, _, _) => { self.uid = Some(uid); // Successful login active_bot.uid = self.uid.clone(); self.backend.send(BKCommand::Sync(None, true)).unwrap(); } BKResponse::Sync(_) => self.backend.send(BKCommand::Sync(None, false)).unwrap(), BKResponse::SyncError(_) => self.backend.send(BKCommand::Sync(None, false)).unwrap(), BKResponse::ShutDown => { return false; } BKResponse::LoginError(x) => { panic!("Error while trying to login: {:#?}", x) } _ => (), } true } fn handle_messages(&mut self, messages: Vec<Message>, active_bot: &ActiveBot) { 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(); // 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 { for handler in self.handlers.iter_mut() { match handler.handle_message(&active_bot, &message) { HandleResult::ContinueHandling => continue, HandleResult::StopHandling => break, } } } } } fn handle_rooms(&self, rooms: Vec<Room>) { for rr in rooms { if rr.membership.is_invited() { self.backend .send(BKCommand::JoinRoom(rr.id.clone())) .unwrap(); println!("Joining room {}", rr.id.clone()); } } } } /// Handle for an active bot that allows sending message, leaving rooms /// and shutting down the bot #[derive(Clone)] pub struct ActiveBot { backend: Sender<BKCommand>, uid: Option<String>, verbose: bool, } impl ActiveBot { /// 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 that 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 html = None; self.raw_send_message(msg,html,room,msgtype); } /// Sends an HTML message to a given room, with a given message-type. /// * msg: The incoming message /// * html: The html-formatted message /// * room: The room-id that the message should be sent to /// * msgtype: Type of message (text or notice) pub fn send_html_message(&self, msg: &str, html: &str, room: &str, msgtype: MessageType) { self.raw_send_message(msg,Some(html),room,msgtype); } fn raw_send_message(&self, msg: &str, html: Option<&str>, room: &str, msgtype: MessageType) { let uid = self.uid.clone().unwrap_or_default(); let date = Local::now(); let mtype = match msgtype { MessageType::RoomNotice => "m.notice".to_string(), MessageType::TextMessage => "m.text".to_string(), }; let (format,formatted_body) = match html { None => (None,None), Some(h) => (Some("org.matrix.custom.html".to_string()),Some(h.to_string())) }; let m = Message { sender: uid, mtype, body: msg.to_string(), room: room.to_string(), date: Local::now(), thumb: None, url: None, id: get_txn_id(room, msg, &date.to_string()), formatted_body, format, in_reply_to: None, receipt: std::collections::HashMap::new(), redacted: false, extra_content: None, source: None, }; if self.verbose { println!("===> sending: {:?}", m); } self.backend.send(BKCommand::SendMsg(m)).unwrap(); } }