├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── botconfig.toml ├── multiple_handlers.rs ├── stateless.rs └── with_state.rs └── src ├── handlers.rs ├── handlers └── stateless_handler.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | examples/botconfig.toml 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "matrix_bot_api" 3 | version = "0.5.1" 4 | authors = ["zwieberl "] 5 | description = """API for writing a bot for the Matrix messaging protocol""" 6 | license = "MIT" 7 | keywords = [ "matrix", "bot", "fractal" ] 8 | repository = "https://github.com/zwieberl/matrix_bot_api" 9 | edition = "2018" 10 | 11 | [dependencies] 12 | fractal-matrix-api = "4.2.0" 13 | serde_json = "1" 14 | 15 | [dependencies.chrono] 16 | features = ["serde"] 17 | version = "0.4.8" 18 | 19 | [dev-dependencies] 20 | config = "0.9.3" 21 | rand = "0.7.0" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 zwieberl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matrix_bot_api 2 | API for writing Matrix-bots (see matrix.org) in rust. 3 | 4 | # Dependencies 5 | It uses the Matrix-API provided by the fractal messenger (fractal-matrix-api). 6 | 7 | # How to use 8 | See the examples-directory 9 | -------------------------------------------------------------------------------- /examples/botconfig.toml: -------------------------------------------------------------------------------- 1 | user = 2 | password = 3 | homeserver_url = 4 | -------------------------------------------------------------------------------- /examples/multiple_handlers.rs: -------------------------------------------------------------------------------- 1 | // This is not a hard dependency. 2 | // Just used for loading the username, password and homeserverurl from a file. 3 | extern crate config; 4 | // Just used for rolling dice 5 | extern crate rand; 6 | 7 | extern crate matrix_bot_api; 8 | use matrix_bot_api::handlers::{ 9 | extract_command, HandleResult, Message, MessageHandler, StatelessHandler, 10 | }; 11 | use matrix_bot_api::{ActiveBot, MatrixBot, MessageType}; 12 | 13 | fn main() { 14 | // ------- Getting the login-credentials from file ------- 15 | // You can get them however you like: hard-code them here, env-variable, 16 | // tcp-connection, read from file, etc. Here, we use the config-crate to 17 | // load from botconfig.toml. 18 | // Change this file to your needs, if you want to use this example binary. 19 | let mut settings = config::Config::default(); 20 | settings 21 | .merge(config::File::with_name("examples/botconfig")) 22 | .unwrap(); 23 | 24 | let user = settings.get_str("user").unwrap(); 25 | let password = settings.get_str("password").unwrap(); 26 | let homeserver_url = settings.get_str("homeserver_url").unwrap(); 27 | // ------------------------------------------------------- 28 | 29 | // Here we want a handler with state (simple counter-variable). 30 | // So we had to implement our own MessageHandler. 31 | let counter = CounterHandler::new(); 32 | 33 | // Give the first handler to your new bot (bot needs at least one handler) 34 | let mut bot = MatrixBot::new(counter); 35 | 36 | // Create another handler, and add it 37 | let mut who = StatelessHandler::new(); 38 | who.register_handle("whoareyou", whoareyou); 39 | 40 | bot.add_handler(who); 41 | 42 | let mut roll = StatelessHandler::new(); 43 | roll.register_handle("roll", roll_dice); 44 | roll.register_handle("help", roll_help); 45 | 46 | bot.add_handler(roll); 47 | 48 | let mut shutdown = StatelessHandler::new(); 49 | // Handlers can have different prefixes of course 50 | shutdown.set_cmd_prefix("BOT: "); 51 | 52 | shutdown.register_handle("leave", |bot, message, _| { 53 | bot.send_message("Bye!", &message.room, MessageType::RoomNotice); 54 | bot.leave_room(&message.room); 55 | HandleResult::StopHandling 56 | }); 57 | 58 | shutdown.register_handle("shutdown", |bot, _message, _| { 59 | bot.shutdown(); 60 | HandleResult::StopHandling 61 | }); 62 | 63 | bot.add_handler(shutdown); 64 | 65 | // Blocking call (until shutdown). Handles all incoming messages and calls the associated functions. 66 | // The bot will automatically join room it is invited to. 67 | bot.run(&user, &password, &homeserver_url); 68 | } 69 | 70 | // We can register multiple handlers. Thus, we create some here. 71 | 72 | // --------- Definition of 1. handler ----------- 73 | // Just copied from with_state.rs: 74 | pub struct CounterHandler { 75 | counter: i32, 76 | } 77 | 78 | impl CounterHandler { 79 | fn new() -> CounterHandler { 80 | CounterHandler { counter: 0 } 81 | } 82 | 83 | fn show_help(&mut self, bot: &ActiveBot, message: &Message) -> HandleResult { 84 | let mut help = "Counter:\n".to_string(); 85 | help += "!incr = Increases counter by one\n"; 86 | help += "!decr = Decreases counter by one\n"; 87 | help += "!show = Show current value of counter\n"; 88 | bot.send_message(&help, &message.room, MessageType::RoomNotice); 89 | HandleResult::ContinueHandling /* There might be more handlers that implement "help" */ 90 | } 91 | } 92 | 93 | impl MessageHandler for CounterHandler { 94 | fn handle_message(&mut self, bot: &ActiveBot, message: &Message) -> HandleResult { 95 | let command = match extract_command(&message.body, "!") { 96 | Some(x) => x, 97 | None => return HandleResult::ContinueHandling, 98 | }; 99 | 100 | match command { 101 | "incr" => self.counter += 1, 102 | "decr" => self.counter -= 1, 103 | "show" => bot.send_message( 104 | &format!("Counter = {}", self.counter), 105 | &message.room, 106 | MessageType::RoomNotice, 107 | ), 108 | "help" => return self.show_help(bot, message), 109 | _ => return HandleResult::ContinueHandling, /* Not a known command */ 110 | } 111 | HandleResult::StopHandling 112 | } 113 | } 114 | 115 | // --------- Definition for 2. handler ----------- 116 | // Copied from stateless.rs 117 | fn whoareyou(bot: &ActiveBot, message: &Message, _cmd: &str) -> HandleResult { 118 | bot.send_message("I'm a bot.", &message.room, MessageType::RoomNotice); 119 | HandleResult::StopHandling 120 | } 121 | 122 | // --------- Definition for 3. handler ----------- 123 | fn roll_help(bot: &ActiveBot, message: &Message, _cmd: &str) -> HandleResult { 124 | let mut help = "Roll dice:\n".to_string(); 125 | help += "!roll X [X ..]\n"; 126 | help += "with\n"; 127 | help += "X = some number. Thats the number of eyes your die will have.\n"; 128 | help += "If multpile numbers are given, multiple dice are rolled. The result as a sum is displayed as well.\n"; 129 | help += "\nExample: !roll 6 12 => Rolls 2 dice, one with 6, the other with 12 eyes.\n"; 130 | bot.send_message(&help, &message.room, MessageType::RoomNotice); 131 | HandleResult::ContinueHandling /* There might be more handlers that implement "help" */ 132 | } 133 | 134 | fn roll_dice(bot: &ActiveBot, message: &Message, cmd: &str) -> HandleResult { 135 | let room = &message.room; 136 | let cmd_split = cmd.split_whitespace(); 137 | 138 | let mut results: Vec = vec![]; 139 | for dice in cmd_split { 140 | let sides = match dice.parse::() { 141 | Ok(x) => x, 142 | Err(_) => { 143 | bot.send_message( 144 | &format!("{} is not a number.", dice), 145 | room, 146 | MessageType::RoomNotice, 147 | ); 148 | return HandleResult::StopHandling; 149 | } 150 | }; 151 | results.push((rand::random::() % sides) + 1); 152 | } 153 | 154 | if results.len() == 0 { 155 | return roll_help(bot, message, cmd); 156 | } 157 | 158 | if results.len() == 1 { 159 | bot.send_message(&format!("{}", results[0]), room, MessageType::RoomNotice); 160 | } else { 161 | // make string from results: 162 | let str_res: Vec = results.iter().map(|x| x.to_string()).collect(); 163 | bot.send_message( 164 | &format!("{} = {}", str_res.join(" + "), results.iter().sum::()), 165 | room, 166 | MessageType::RoomNotice, 167 | ); 168 | } 169 | 170 | HandleResult::StopHandling 171 | } 172 | -------------------------------------------------------------------------------- /examples/stateless.rs: -------------------------------------------------------------------------------- 1 | // This is not a hard dependency. 2 | // Just used for loading the username, password and homeserverurl from a file. 3 | extern crate config; 4 | 5 | extern crate matrix_bot_api; 6 | use matrix_bot_api::handlers::{HandleResult, Message, StatelessHandler}; 7 | use matrix_bot_api::{ActiveBot, MatrixBot, MessageType}; 8 | 9 | // Handle that prints "I'm a bot." as a room-notice on command !whoareyou 10 | fn whoareyou(bot: &ActiveBot, message: &Message, _tail: &str) -> HandleResult { 11 | bot.send_message("I'm a bot.", &message.room, MessageType::RoomNotice); 12 | HandleResult::StopHandling 13 | } 14 | 15 | fn main() { 16 | // ------- Getting the login-credentials from file ------- 17 | // You can get them however you like: hard-code them here, env-variable, 18 | // tcp-connection, read from file, etc. Here, we use the config-crate to 19 | // load from botconfig.toml. 20 | // Change this file to your needs, if you want to use this example binary. 21 | let mut settings = config::Config::default(); 22 | settings 23 | .merge(config::File::with_name("examples/botconfig")) 24 | .unwrap(); 25 | 26 | let user = settings.get_str("user").unwrap(); 27 | let password = settings.get_str("password").unwrap(); 28 | let homeserver_url = settings.get_str("homeserver_url").unwrap(); 29 | // ------------------------------------------------------- 30 | 31 | // Our functions won't have a state, so a stateless handler is what we want. 32 | // To keep it simple, matrix_bot_api does provide one for you: 33 | let mut handler = StatelessHandler::new(); 34 | 35 | // Register a handle. The function whoareyou() will be called when a user 36 | // types !whoareyou into the chat 37 | handler.register_handle("whoareyou", whoareyou); 38 | 39 | // Register handle that lets the bot leave the current room on !leave. 40 | // We can also use closures that do not capture here. 41 | handler.register_handle("leave", |bot, message, _tail| { 42 | bot.send_message("Bye!", &message.room, MessageType::RoomNotice); 43 | bot.leave_room(&message.room); 44 | HandleResult::StopHandling 45 | }); 46 | 47 | // Simply echo what was given to you by !echo XY (will print only "Echo: XY", !echo is stripped) 48 | handler.register_handle("echo", |bot, message, tail| { 49 | bot.send_message( 50 | &format!("Echo: {}", tail), 51 | &message.room, 52 | MessageType::TextMessage, 53 | ); 54 | HandleResult::StopHandling 55 | }); 56 | 57 | // Shutdown on !shutdown. This does not leave any rooms. 58 | handler.register_handle("shutdown", |bot, _room, _cmd| { 59 | bot.shutdown(); 60 | HandleResult::StopHandling 61 | }); 62 | 63 | // Give the handler to your new bot 64 | let mut bot = MatrixBot::new(handler); 65 | 66 | // Optional: To get all Matrix-message coming in and going out (quite verbose!) 67 | bot.set_verbose(true); 68 | 69 | // Blocking call (until shutdown). Handles all incoming messages and calls the associated functions. 70 | // The bot will automatically join room it is invited to. 71 | bot.run(&user, &password, &homeserver_url); 72 | } 73 | -------------------------------------------------------------------------------- /examples/with_state.rs: -------------------------------------------------------------------------------- 1 | // This is not a hard dependency. 2 | // Just used for loading the username, password and homeserverurl from a file. 3 | extern crate config; 4 | 5 | extern crate matrix_bot_api; 6 | use matrix_bot_api::handlers::{extract_command, HandleResult, Message, MessageHandler}; 7 | use matrix_bot_api::{ActiveBot, MatrixBot, MessageType}; 8 | 9 | // Our handler wants a mutable state (here represented by a little counter-variable) 10 | // This counter can be increased or decreased by users giving the bot a command. 11 | pub struct CounterHandler { 12 | counter: i32, 13 | } 14 | 15 | impl CounterHandler { 16 | fn new() -> CounterHandler { 17 | CounterHandler { counter: 0 } 18 | } 19 | } 20 | 21 | // Implement the trait MessageHandler, to be able to give it to our MatrixBot. 22 | // This trait only has one function: handle_message() and will be called on each 23 | // new (text-)message in the room the bot is in. 24 | impl MessageHandler for CounterHandler { 25 | fn handle_message(&mut self, bot: &ActiveBot, message: &Message) -> HandleResult { 26 | // extract_command() will split the message by whitespace and remove the prefix (here "!") 27 | // from the first entry. If the message does not start with the given prefix, None is returned. 28 | let command = match extract_command(&message.body, "!") { 29 | Some(x) => x, 30 | None => return HandleResult::ContinueHandling, 31 | }; 32 | 33 | // Now we have the current command (some text prefixed with our prefix !) 34 | // Your handler could have a HashMap with the command as the key 35 | // and a specific function for it (like StatelessHandler does it), 36 | // or you can use a simple match-statement, to act on the given command: 37 | match command { 38 | "incr" => self.counter += 1, 39 | "decr" => self.counter -= 1, 40 | "show" => bot.send_message( 41 | &format!("Counter = {}", self.counter), 42 | &message.room, 43 | MessageType::RoomNotice, 44 | ), 45 | "shutdown" => bot.shutdown(), 46 | _ => return HandleResult::ContinueHandling, /* Not a known command */ 47 | } 48 | HandleResult::StopHandling 49 | } 50 | } 51 | 52 | fn main() { 53 | // ------- Getting the login-credentials from file ------- 54 | // You can get them however you like: hard-code them here, env-variable, 55 | // tcp-connection, read from file, etc. Here, we use the config-crate to 56 | // load from botconfig.toml. 57 | // Change this file to your needs, if you want to use this example binary. 58 | let mut settings = config::Config::default(); 59 | settings 60 | .merge(config::File::with_name("examples/botconfig")) 61 | .unwrap(); 62 | 63 | let user = settings.get_str("user").unwrap(); 64 | let password = settings.get_str("password").unwrap(); 65 | let homeserver_url = settings.get_str("homeserver_url").unwrap(); 66 | // ------------------------------------------------------- 67 | 68 | // Here we want a handler with state (simple counter-variable). 69 | // So we had to implement our own MessageHandler. 70 | let handler = CounterHandler::new(); 71 | 72 | // Give the handler to your new bot 73 | let bot = MatrixBot::new(handler); 74 | 75 | // Blocking call (until shutdown). Handles all incoming messages and calls the associated functions. 76 | // The bot will automatically join room it is invited to. 77 | bot.run(&user, &password, &homeserver_url); 78 | } 79 | -------------------------------------------------------------------------------- /src/handlers.rs: -------------------------------------------------------------------------------- 1 | pub use fractal_matrix_api::types::Message; 2 | 3 | /// What to do after finished handling a message 4 | pub enum HandleResult { 5 | /// Give this message to the next MessageHandler as well 6 | ContinueHandling, 7 | /// Stop handling this message 8 | StopHandling, 9 | } 10 | 11 | /// Any struct that implements this trait can be passed to a MatrixBot. 12 | /// The bot will call `handle_message()` on each arriving text-message 13 | /// The result HandleResult defines if `handle_message()` of other handlers will 14 | /// be called with this message or not. 15 | /// 16 | /// The bot will also call `init_handler()` on startup to allow handlers to 17 | /// setup any background work 18 | pub trait MessageHandler { 19 | /// Will be called for every text message send to a room the bot is in 20 | fn handle_message(&mut self, bot: &ActiveBot, message: &Message) -> HandleResult; 21 | 22 | /// Will be called once the bot has started 23 | fn init_handler(&mut self, _bot: &ActiveBot) {} 24 | } 25 | 26 | /// Convenience-function to split the incoming message by whitespace and 27 | /// extract the given prefix from the first word. 28 | /// Returns None, if the message does not start with the given prefix 29 | /// # Example: 30 | /// extract_command("!roll 6", "!") will return Some("roll") 31 | /// extract_command("Hi all!", "!") will return None 32 | pub fn extract_command<'a>(message: &'a str, prefix: &str) -> Option<&'a str> { 33 | if message.starts_with(prefix) { 34 | let new_start = prefix.len(); 35 | let key = message[new_start..].split_whitespace().next().unwrap_or(""); 36 | return Some(&key); 37 | } 38 | None 39 | } 40 | 41 | pub mod stateless_handler; 42 | pub use self::stateless_handler::StatelessHandler; 43 | 44 | use crate::ActiveBot; 45 | -------------------------------------------------------------------------------- /src/handlers/stateless_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::handlers::{extract_command, HandleResult, Message, MessageHandler}; 2 | use crate::ActiveBot; 3 | use std::collections::HashMap; 4 | 5 | /// Convenience-handler that can quickly register and call functions 6 | /// without any state (each function-call will result in the same output) 7 | pub struct StatelessHandler { 8 | cmd_prefix: String, 9 | cmd_handles: HashMap HandleResult>, 10 | } 11 | 12 | impl StatelessHandler { 13 | pub fn new() -> StatelessHandler { 14 | StatelessHandler { 15 | cmd_prefix: "!".to_string(), 16 | cmd_handles: HashMap::new(), 17 | } 18 | } 19 | 20 | /// With what prefix commands to the bot will start 21 | /// Default: "!" 22 | pub fn set_cmd_prefix(&mut self, prefix: &str) { 23 | self.cmd_prefix = prefix.to_string(); 24 | } 25 | 26 | /// Register handles 27 | /// * command: For which command (excluding the prefix!) the handler should be called 28 | /// * handler: The handler to be called if the given command was received in the room 29 | /// 30 | /// Handler-function: 31 | /// * bot: This bot 32 | /// * message: The message from fractal, containing the room the command was sent in, message body, etc. 33 | /// * tail: The message-body without prefix and command (e.g. "!roll 12" -> "12") 34 | /// 35 | /// # Example 36 | /// handler.set_cmd_prefix("BOT:") 37 | /// handler.register_handle("sayhi", foo); 38 | /// foo() will be called, when BOT:sayhi is received by the bot 39 | pub fn register_handle( 40 | &mut self, 41 | command: &str, 42 | handler: fn(bot: &ActiveBot, message: &Message, tail: &str) -> HandleResult, 43 | ) { 44 | self.cmd_handles.insert(command.to_string(), handler); 45 | } 46 | } 47 | 48 | impl MessageHandler for StatelessHandler { 49 | fn handle_message(&mut self, bot: &ActiveBot, message: &Message) -> HandleResult { 50 | match extract_command(&message.body, &self.cmd_prefix) { 51 | Some(command) => { 52 | let func = self.cmd_handles.get(command).map(|x| *x); 53 | match func { 54 | Some(func) => { 55 | if bot.verbose { 56 | println!("Found handle for command \"{}\". Calling it.", &command); 57 | } 58 | let end_of_prefix = self.cmd_prefix.len() + command.len(); 59 | func(bot, message, &message.body[end_of_prefix..]) 60 | } 61 | None => { 62 | if bot.verbose { 63 | println!("Command \"{}\" not found in registered handles", &command); 64 | } 65 | HandleResult::ContinueHandling 66 | } 67 | } 68 | } 69 | None => { 70 | HandleResult::ContinueHandling /* Doing nothing. Not for us */ 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # matrix_bot_api 2 | //! Easy to use API for implementing your own Matrix-Bot (see matrix.org) 3 | //! 4 | //! # Basic setup: 5 | //! There are two main parts: A [`MessageHandler`] and the [`MatrixBot`]. 6 | //! The MessageHandler defines what happens with received messages. 7 | //! The MatrixBot consumes your MessageHandler and deals with all 8 | //! the matrix-protocol-stuff, calling your MessageHandler for each 9 | //! new text-message with an [`ActiveBot`] handle that allows the handler to 10 | //! respond to the message. 11 | //! 12 | //! You can write your own MessageHandler by implementing the [`MessageHandler`]-trait, 13 | //! or use one provided by this crate (currently only [`StatelessHandler`]). 14 | //! 15 | //! # Multple Handlers: 16 | //! One can register multiple MessageHandlers with a bot. Thus one can "plug and play" 17 | //! different features to ones MatrixBot. 18 | //! Messages are given to each handler in the order of their registration. 19 | //! A message is given to the next handler until one handler returns `StopHandling`. 20 | //! Thus a message can be handled by multiple handlers as well (for example for "help"). 21 | //! 22 | //! # Example 23 | //! ``` 24 | //! extern crate matrix_bot_api; 25 | //! use matrix_bot_api::{MatrixBot, MessageType}; 26 | //! use matrix_bot_api::handlers::{StatelessHandler, HandleResult}; 27 | //! 28 | //! fn main() { 29 | //! let mut handler = StatelessHandler::new(); 30 | //! handler.register_handle("shutdown", |bot, _, _| { 31 | //! bot.shutdown(); 32 | //! HandleResult::ContinueHandling /* Other handlers might need to clean up after themselves on shutdown */ 33 | //! }); 34 | //! 35 | //! handler.register_handle("echo", |bot, message, tail| { 36 | //! bot.send_message(&format!("Echo: {}", tail), &message.room, MessageType::TextMessage); 37 | //! HandleResult::StopHandling 38 | //! }); 39 | //! 40 | //! let mut bot = MatrixBot::new(handler); 41 | //! bot.run("your_bot", "secret_password", "https://your.homeserver"); 42 | //! } 43 | //! ``` 44 | //! Have a look in the examples/ directory for detailed examples. 45 | //! 46 | //! [`MatrixBot`]: struct.MatrixBot.html 47 | //! [`ActiveBot`]: struct.ActiveBot.html 48 | //! [`MessageHandler`]: handlers/trait.MessageHandler.html 49 | //! [`StatelessHandler`]: handlers/stateless_handler/struct.StatelessHandler.html 50 | use chrono::prelude::*; 51 | 52 | use serde_json::json; 53 | use serde_json::value::Value as JsonValue; 54 | 55 | use fractal_matrix_api::backend::BKCommand; 56 | use fractal_matrix_api::backend::BKResponse; 57 | use fractal_matrix_api::backend::Backend; 58 | use fractal_matrix_api::types::message::get_txn_id; 59 | pub use fractal_matrix_api::types::{Message, Room}; 60 | 61 | use std::sync::mpsc::channel; 62 | use std::sync::mpsc::{Receiver, Sender}; 63 | 64 | pub mod handlers; 65 | use handlers::{HandleResult, MessageHandler}; 66 | 67 | /// How messages from the bot should be formatted. This is up to the client, 68 | /// but usually RoomNotice's have a different color than TextMessage's. 69 | pub enum MessageType { 70 | RoomNotice, 71 | TextMessage, 72 | Image, 73 | } 74 | 75 | pub struct MatrixBot { 76 | backend: Sender, 77 | rx: Receiver, 78 | uid: Option, 79 | verbose: bool, 80 | update_read_marker: bool, 81 | handlers: Vec>, 82 | } 83 | 84 | impl MatrixBot { 85 | /// Consumes any struct that implements the MessageHandler-trait. 86 | pub fn new(handler: M) -> MatrixBot 87 | where 88 | M: handlers::MessageHandler + 'static + Send, 89 | { 90 | let (tx, rx): (Sender, Receiver) = channel(); 91 | let bk = Backend::new(tx); 92 | // Here it would be ideal to extend fractal_matrix_api in order to be able to give 93 | // sync a limit-parameter. 94 | // Until then, the workaround is to send "since" of the backend to "now". 95 | // Not interested in any messages since login 96 | bk.data.lock().unwrap().since = Some(Local::now().to_string()); 97 | MatrixBot { 98 | backend: bk.run(), 99 | rx, 100 | uid: None, 101 | verbose: false, 102 | update_read_marker: true, 103 | handlers: vec![Box::new(handler)], 104 | } 105 | } 106 | 107 | /// Create a copy of the internal ActiveBot instance for sending messages 108 | pub fn get_activebot_clone(&self) -> ActiveBot { 109 | ActiveBot { 110 | backend: self.backend.clone(), 111 | uid: self.uid.clone(), 112 | verbose: self.verbose, 113 | } 114 | } 115 | 116 | /// Add an additional handler. 117 | /// Each message will be given to all registered handlers until 118 | /// one of them returns "HandleResult::StopHandling". 119 | pub fn add_handler(&mut self, handler: M) 120 | where 121 | M: handlers::MessageHandler + 'static + Send, 122 | { 123 | self.handlers.push(Box::new(handler)); 124 | } 125 | 126 | /// If true, will print all Matrix-message coming in and going out (quite verbose!) to stdout 127 | /// Default: false 128 | pub fn set_verbose(&mut self, verbose: bool) { 129 | self.verbose = verbose; 130 | } 131 | 132 | /// If true, bot will continually update its read marker 133 | /// Default: true 134 | pub fn set_update_read_marker(&mut self, update_read_marker: bool) { 135 | self.update_read_marker = update_read_marker; 136 | } 137 | 138 | /// Blocking call that runs as long as the Bot is running. 139 | /// Will call for each incoming text-message the given MessageHandler. 140 | /// Bot will automatically join all rooms it is invited to. 141 | /// Will return on shutdown only. 142 | /// All messages prior to run() will be ignored. 143 | pub fn run(mut self, user: &str, password: &str, homeserver_url: &str) { 144 | self.backend 145 | .send(BKCommand::Login( 146 | user.to_string(), 147 | password.to_string(), 148 | homeserver_url.to_string(), 149 | )) 150 | .unwrap(); 151 | 152 | let mut active_bot = self.get_activebot_clone(); 153 | 154 | for handler in self.handlers.iter_mut() { 155 | handler.init_handler(&active_bot); 156 | } 157 | 158 | loop { 159 | let cmd = self.rx.recv().unwrap(); 160 | if !self.handle_recvs(cmd, &mut active_bot) { 161 | break; 162 | } 163 | } 164 | } 165 | 166 | /* --------- Private functions ------------ */ 167 | fn handle_recvs(&mut self, resp: BKResponse, active_bot: &mut ActiveBot) -> bool { 168 | if self.verbose { 169 | println!("<=== received: {:?}", resp); 170 | } 171 | 172 | match resp { 173 | BKResponse::UpdateRooms(x) => self.handle_rooms(x), 174 | //BKResponse::Rooms(x, _) => self.handle_rooms(x), 175 | BKResponse::RoomMessages(x) => self.handle_messages(x, active_bot), 176 | BKResponse::Token(uid, _, _) => { 177 | self.uid = Some(uid); // Successful login 178 | active_bot.uid = self.uid.clone(); 179 | self.backend.send(BKCommand::Sync(None, true)).unwrap(); 180 | } 181 | BKResponse::Sync(_) => self.backend.send(BKCommand::Sync(None, false)).unwrap(), 182 | BKResponse::SyncError(_) => self.backend.send(BKCommand::Sync(None, false)).unwrap(), 183 | BKResponse::ShutDown => { 184 | return false; 185 | } 186 | BKResponse::LoginError(x) => panic!("Error while trying to login: {:#?}", x), 187 | _ => (), 188 | } 189 | true 190 | } 191 | 192 | fn handle_messages(&mut self, messages: Vec, active_bot: &ActiveBot) { 193 | for message in messages { 194 | 195 | /* First of all, mark all new messages as "read" */ 196 | if self.update_read_marker { 197 | self.backend 198 | .send(BKCommand::MarkAsRead( 199 | message.room.clone(), 200 | message.id.clone(), 201 | )) 202 | .unwrap(); 203 | } 204 | 205 | // It might be a command for us, if the message is text 206 | // and if its not from the bot itself 207 | let uid = self.uid.clone().unwrap_or_default(); 208 | // This might be a command for us (only text-messages are interesting) 209 | if message.mtype == "m.text" && message.sender != uid { 210 | for handler in self.handlers.iter_mut() { 211 | match handler.handle_message(&active_bot, &message) { 212 | HandleResult::ContinueHandling => continue, 213 | HandleResult::StopHandling => break, 214 | } 215 | } 216 | } 217 | } 218 | } 219 | 220 | fn handle_rooms(&self, rooms: Vec) { 221 | for rr in rooms { 222 | if rr.membership.is_invited() { 223 | self.backend 224 | .send(BKCommand::JoinRoom(rr.id.clone())) 225 | .unwrap(); 226 | println!("Joining room {}", rr.id.clone()); 227 | } 228 | } 229 | } 230 | } 231 | 232 | /// Handle for an active bot that allows sending message, leaving rooms 233 | /// and shutting down the bot 234 | #[derive(Clone)] 235 | pub struct ActiveBot { 236 | backend: Sender, 237 | uid: Option, 238 | verbose: bool, 239 | } 240 | 241 | impl ActiveBot { 242 | /// Will shutdown the bot. The bot will not leave any rooms. 243 | pub fn shutdown(&self) { 244 | self.backend.send(BKCommand::ShutDown).unwrap(); 245 | } 246 | 247 | /// Will leave the given room (give room-id, not room-name) 248 | pub fn leave_room(&self, room_id: &str) { 249 | self.backend 250 | .send(BKCommand::LeaveRoom(room_id.to_string())) 251 | .unwrap(); 252 | } 253 | 254 | /// Sends a message to a given room, with a given message-type. 255 | /// * msg: The incoming message 256 | /// * room: The room-id that the message should be sent to 257 | /// * msgtype: Type of message (text or notice) 258 | pub fn send_message(&self, msg: &str, room: &str, msgtype: MessageType) { 259 | let html = None; 260 | self.raw_send_message(msg, html, None, None, room, msgtype); 261 | } 262 | /// Sends an HTML message to a given room, with a given message-type. 263 | /// * msg: The incoming message 264 | /// * html: The html-formatted message 265 | /// * room: The room-id that the message should be sent to 266 | /// * msgtype: Type of message (text or notice) 267 | pub fn send_html_message(&self, msg: &str, html: &str, room: &str, msgtype: MessageType) { 268 | self.raw_send_message(msg, Some(html), None, None, room, msgtype); 269 | } 270 | 271 | /// Sends an image to a given room. 272 | /// * name: The name of the image 273 | /// * url: The url for the image 274 | /// * room: The room-id that the message should be sent to 275 | pub fn send_image( 276 | &self, 277 | name: &str, 278 | url: &str, 279 | width: i32, 280 | height: i32, 281 | size: i32, 282 | mime_type: &str, 283 | room: &str, 284 | ) { 285 | let raw = json!({"info": { 286 | "h": height, 287 | "mimetype":mime_type, 288 | "size":size, 289 | "w":width 290 | }}); 291 | 292 | self.raw_send_message( 293 | name, 294 | None, 295 | Some(url), 296 | Some(raw), 297 | room, 298 | MessageType::Image, 299 | ) 300 | } 301 | 302 | fn raw_send_message( 303 | &self, 304 | msg: &str, 305 | html: Option<&str>, 306 | url: Option<&str>, 307 | extra_content: Option, 308 | room: &str, 309 | msgtype: MessageType, 310 | ) { 311 | let uid = self.uid.clone().unwrap_or_default(); 312 | let date = Local::now(); 313 | let mtype = match msgtype { 314 | MessageType::RoomNotice => "m.notice".to_string(), 315 | MessageType::TextMessage => "m.text".to_string(), 316 | MessageType::Image => "m.image".to_string(), 317 | }; 318 | 319 | let (format, formatted_body) = match html { 320 | None => (None, None), 321 | Some(h) => ( 322 | Some("org.matrix.custom.html".to_string()), 323 | Some(h.to_string()), 324 | ), 325 | }; 326 | 327 | let u = match url { 328 | None => None, 329 | Some(a) => Some(a.to_string()), 330 | }; 331 | 332 | let m = Message { 333 | sender: uid, 334 | mtype, 335 | body: msg.to_string(), 336 | room: room.to_string(), 337 | date: Local::now(), 338 | thumb: None, 339 | url: u, 340 | id: get_txn_id(room, msg, &date.to_string()), 341 | formatted_body, 342 | format, 343 | in_reply_to: None, 344 | receipt: std::collections::HashMap::new(), 345 | redacted: false, 346 | extra_content: extra_content, 347 | source: None, 348 | }; 349 | 350 | if self.verbose { 351 | println!("===> sending: {:?}", m); 352 | } 353 | 354 | self.backend.send(BKCommand::SendMsg(m)).unwrap(); 355 | } 356 | } 357 | --------------------------------------------------------------------------------