├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── appservice.rs ├── convert.rs ├── lib.rs ├── mappingdict.rs ├── matrix.rs ├── request.rs ├── server.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "matrix-appservice-rs" 3 | description = "A library to aid in creating Matrix appservices." 4 | version = "0.4.0" 5 | license = "MIT" 6 | repository = "https://github.com/lieuwex/matrix-appservice-rs/" 7 | authors = ["Lieuwe Rooijakkers "] 8 | edition = "2018" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [package.metadata.docs.rs] 13 | all-features = true 14 | 15 | [features] 16 | default = [ "convert", "serve" ] 17 | convert = [ "lol_html", "regex", "pcre2" ] 18 | serve = [ "hyper/server", "hyper/http1", "hyper/tcp", "bytes" ] 19 | 20 | [dependencies] 21 | ruma = { version = "0.1.0", features = [ "appservice-api-s" ] } 22 | ruma-client = { version = "0.5.0" } 23 | 24 | serde = "1" 25 | serde_json = "1.0" 26 | 27 | hyper = "0.14" 28 | bytes = { version = "1", optional = true } 29 | 30 | rand = { version = "0.8", optional = true } 31 | 32 | lol_html = { version = "0.3.0", optional = true } 33 | regex = { version = "1", optional = true } 34 | pcre2 = { version = "0.2.3", optional = true } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2020 Lieuwe Rooijakkers et al 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | matrix-appservice-rs 2 | === 3 | 4 | Work in progress. 5 | 6 | A library to aid in creating Matrix appservices using Rust. 7 | The library exposes useful types that build on top of Ruma. 8 | -------------------------------------------------------------------------------- /src/appservice.rs: -------------------------------------------------------------------------------- 1 | use ruma::api::exports::http::Uri; 2 | use ruma::identifiers::ServerName; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | pub use ruma::api::appservice::{Namespace, Namespaces, Registration, RegistrationInit}; 7 | 8 | #[cfg(feature = "rand")] 9 | pub fn new_registration_rand( 10 | id: String, 11 | namespaces: Namespaces, 12 | sender_localpart: String, 13 | url: Uri, 14 | rate_limited: bool, 15 | ) -> Registration { 16 | use crate::util::random_alphanumeric; 17 | Registration::from(RegistrationInit { 18 | id, 19 | as_token: random_alphanumeric(64), 20 | hs_token: random_alphanumeric(64), 21 | namespaces, 22 | url: url.to_string(), 23 | sender_localpart, 24 | rate_limited: Some(rate_limited), 25 | protocols: None, 26 | }) 27 | } 28 | 29 | /// A struct containing information required by an application service. 30 | #[derive(Clone, Debug, Serialize, Deserialize)] 31 | pub struct ApplicationService { 32 | server_name: Box, 33 | server_url: String, 34 | } 35 | 36 | impl ApplicationService { 37 | /// Create a new ApplicationService struct with the given information. 38 | pub fn new(server_name: Box, server_url: Uri) -> Self { 39 | Self { 40 | server_name, 41 | server_url: server_url.to_string(), 42 | } 43 | } 44 | 45 | /// Get a reference to the server name in this ApplicationService instance. 46 | pub fn server_name(&self) -> &ServerName { 47 | self.server_name.as_ref() 48 | } 49 | /// Get a reference to the server url in this ApplicationService instance. 50 | pub fn server_url(&self) -> Uri { 51 | self.server_url.parse().unwrap() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/convert.rs: -------------------------------------------------------------------------------- 1 | pub mod to_external { 2 | use std::borrow::Cow; 3 | use std::collections::HashMap; 4 | use std::convert::TryFrom; 5 | 6 | use lol_html::rewrite_str; 7 | use ruma::identifiers::{RoomAliasId, UserId}; 8 | 9 | pub use lol_html::{ 10 | html_content::{ContentType, Element}, 11 | ElementContentHandlers, Settings, 12 | }; 13 | 14 | type UserMapper<'a> = &'a dyn Fn(UserId, &Info) -> Option; 15 | type RoomMapper<'a> = &'a dyn Fn(RoomAliasId, &Info) -> Option; 16 | type ElementClosure<'a> = &'a dyn Fn(&mut Element<'_, '_>, &Info); 17 | 18 | pub struct Info<'a> { 19 | user_mapper: UserMapper<'a>, 20 | room_mapper: RoomMapper<'a>, 21 | element_handlers: HashMap>, 22 | } 23 | 24 | pub fn generate_user_mapper_from_hashmap( 25 | map: HashMap, 26 | ) -> impl Fn(UserId, &Info) -> Option { 27 | move |user_id: UserId, _: &Info| -> Option { map.get(&user_id).cloned() } 28 | } 29 | 30 | pub fn generate_room_mapper_from_hashmap( 31 | map: HashMap, 32 | ) -> impl Fn(RoomAliasId, &Info) -> Option { 33 | move |room_id: RoomAliasId, _: &Info| -> Option { map.get(&room_id).cloned() } 34 | } 35 | 36 | impl<'a> Info<'a> { 37 | pub fn new() -> Self { 38 | Self { 39 | user_mapper: &|_: UserId, _: &Info| None, 40 | room_mapper: &|_: RoomAliasId, _: &Info| None, 41 | element_handlers: HashMap::new(), 42 | } 43 | } 44 | 45 | pub fn user_mapper(&mut self, f: &'a dyn Fn(UserId, &Info) -> Option) -> &mut Self { 46 | self.user_mapper = f; 47 | self 48 | } 49 | 50 | pub fn room_mapper( 51 | &mut self, 52 | f: &'a dyn Fn(RoomAliasId, &Info) -> Option, 53 | ) -> &mut Self { 54 | self.room_mapper = f; 55 | self 56 | } 57 | 58 | pub fn add_element_handler(&mut self, element: String, f: ElementClosure<'a>) -> &mut Self { 59 | self.element_handlers.insert(element, f); 60 | self 61 | } 62 | } 63 | 64 | impl Default for Info<'_> { 65 | fn default() -> Self { 66 | Info::new() 67 | } 68 | } 69 | 70 | fn stringify_a_tag(el: &mut Element, info: &Info) { 71 | let normal = |el: &mut Element, url: Option| match info.element_handlers.get("a") { 72 | Some(f) => { 73 | f(el, info); 74 | } 75 | None => { 76 | el.remove_and_keep_content(); 77 | 78 | if let Some(url) = url { 79 | el.prepend("[", ContentType::Html); 80 | el.append(&format!("]({})", url), ContentType::Html); 81 | } 82 | } 83 | }; 84 | 85 | let href = match el.get_attribute("href") { 86 | Some(href) => href, 87 | _ => return normal(el, None), 88 | }; 89 | 90 | let mentioned = match href.strip_prefix("https://matrix.to/#/") { 91 | None => return normal(el, Some(href)), 92 | Some(suffix) => suffix, 93 | }; 94 | 95 | let s = match mentioned.chars().next() { 96 | Some('@') => { 97 | let mentioned = UserId::try_from(mentioned).unwrap(); 98 | (info.user_mapper)(mentioned, info) 99 | } 100 | Some('#') => { 101 | let room = RoomAliasId::try_from(mentioned).unwrap(); 102 | (info.room_mapper)(room, info) 103 | } 104 | _ => None, 105 | }; 106 | 107 | if let Some(s) = s { 108 | el.replace(&s, ContentType::Html) 109 | } else { 110 | normal(el, Some(href)) 111 | } 112 | } 113 | 114 | pub fn convert(s: &str, info: &Info) -> Result { 115 | let mut settings = Settings::default(); 116 | 117 | settings.element_content_handlers = vec![ 118 | (( 119 | Cow::Owned("body".parse().unwrap()), 120 | ElementContentHandlers::default().comments(|c| { 121 | c.remove(); 122 | Ok(()) 123 | }), 124 | )), 125 | (( 126 | Cow::Owned("a".parse().unwrap()), 127 | ElementContentHandlers::default().element(|e| { 128 | stringify_a_tag(e, info); 129 | Ok(()) 130 | }), 131 | )), 132 | (( 133 | Cow::Owned("*".parse().unwrap()), 134 | ElementContentHandlers::default().element(|e| { 135 | let tag = e.tag_name(); 136 | if let Some(handler) = info.element_handlers.get(&tag) { 137 | handler(e, info); 138 | } else { 139 | e.remove_and_keep_content(); 140 | } 141 | Ok(()) 142 | }), 143 | )), 144 | ]; 145 | 146 | Ok(rewrite_str(s, settings).unwrap()) 147 | } 148 | 149 | #[cfg(test)] 150 | mod tests { 151 | use std::collections::HashMap; 152 | use std::convert::TryFrom; 153 | 154 | use lol_html::html_content::ContentType; 155 | use ruma::identifiers::{user_id, RoomAliasId, UserId}; 156 | 157 | use crate::convert::to_external::{convert, Element, Info}; 158 | 159 | #[test] 160 | fn test_stripping() { 161 | let info = Info::new(); 162 | 163 | let before = "kaas".to_string(); 164 | let after = convert(&before, &info).unwrap(); 165 | assert_eq!(after, "kaas"); 166 | } 167 | 168 | #[test] 169 | fn test_anchor() { 170 | let mut user_mapping = HashMap::new(); 171 | user_mapping.insert(user_id!("@tomsg_tom:lieuwe.xyz"), "tom".to_string()); 172 | 173 | let mut info = Info::new(); 174 | let f = move |user_id: UserId, _: &Info| user_mapping.get(&user_id).cloned(); 175 | info.user_mapper(&f); 176 | 177 | let before = 178 | "tom (tomsg)".to_string(); 179 | 180 | let after = convert(&before, &info).unwrap(); 181 | assert_eq!(after, "tom"); 182 | } 183 | 184 | #[test] 185 | fn test_anchor_room() { 186 | let mut room_mapping = HashMap::new(); 187 | room_mapping.insert( 188 | RoomAliasId::try_from("#tomsg:lieuwe.xyz").unwrap(), 189 | "tomsg".to_string(), 190 | ); 191 | 192 | let mut info = Info::new(); 193 | let f = move |room_id: RoomAliasId, _: &Info| room_mapping.get(&room_id).cloned(); 194 | info.room_mapper(&f); 195 | 196 | let before = "tomsg".to_string(); 197 | 198 | let after = convert(&before, &info).unwrap(); 199 | assert_eq!(after, "tomsg"); 200 | } 201 | 202 | #[test] 203 | fn test_complex() { 204 | let mut user_mapping = HashMap::new(); 205 | user_mapping.insert(user_id!("@tomsg_tom:lieuwe.xyz"), "tom".to_string()); 206 | user_mapping.insert(user_id!("@lieuwe:lieuwe.xyz"), "lieuwe".to_string()); 207 | 208 | let mut info = Info::new(); 209 | let f = move |user_id: UserId, _: &Info| user_mapping.get(&user_id).cloned(); 210 | info.user_mapper(&f); 211 | 212 | let before = 213 | "tom (tomsg): How're you doing, greetings henk. Btw, here is a cool link bing".to_string(); 214 | 215 | let after = convert(&before, &info).unwrap(); 216 | assert_eq!( 217 | after, 218 | "tom: How're you doing, greetings lieuwe. Btw, here is a cool link [bing](google.nl)" 219 | ); 220 | } 221 | 222 | #[test] 223 | fn test_element_handlers() { 224 | let mut info = Info::new(); 225 | info.add_element_handler("a".to_string(), &|el: &mut Element<'_, '_>, _: &Info| { 226 | el.replace("test", ContentType::Html); 227 | }); 228 | 229 | let before = "this will be gone"; 230 | 231 | let after = convert(&before, &info).unwrap(); 232 | assert_eq!(after, "test"); 233 | } 234 | 235 | #[test] 236 | fn test_complex2() { 237 | let before = "
In reply to @tomsg_tom:lieuwe.xyz
⛄️
Hallo tom (tomsg) dit is een test kaas ham coole site"; 238 | let after = "Hallo tom dit is een test *kaas* **ham** [coole site](http://tomsmeding.com/f/kaas.png)"; 239 | 240 | let mut info = Info::new(); 241 | 242 | info.add_element_handler( 243 | "mx-reply".to_string(), 244 | &|el: &mut Element<'_, '_>, _: &Info| el.remove(), 245 | ); 246 | info.add_element_handler("em".to_string(), &|el: &mut Element<'_, '_>, _: &Info| { 247 | el.prepend("*", lol_html::html_content::ContentType::Html); 248 | el.remove_and_keep_content(); 249 | el.append("*", lol_html::html_content::ContentType::Html); 250 | }); 251 | info.add_element_handler( 252 | "strong".to_string(), 253 | &|el: &mut Element<'_, '_>, _: &Info| { 254 | el.prepend("**", lol_html::html_content::ContentType::Html); 255 | el.remove_and_keep_content(); 256 | el.append("**", lol_html::html_content::ContentType::Html); 257 | }, 258 | ); 259 | 260 | let mut user_mapping: HashMap = HashMap::new(); 261 | user_mapping.insert(user_id!("@tomsg_tom:lieuwe.xyz"), "tom".to_string()); 262 | 263 | let f = move |user_id: UserId, _: &Info| user_mapping.get(&user_id).cloned(); 264 | info.user_mapper(&f); 265 | 266 | assert_eq!(after, convert(&before, &info).unwrap()); 267 | } 268 | } 269 | } 270 | 271 | pub mod to_matrix { 272 | use std::collections::HashMap; 273 | 274 | use crate::matrix::MatrixToItem; 275 | 276 | // HACK 277 | use regex::escape; 278 | 279 | use pcre2::bytes::Regex; 280 | 281 | pub struct Info<'a> { 282 | pub map: HashMap>, 283 | } 284 | 285 | pub struct BuiltRegex(Regex); 286 | 287 | pub fn build_regex(info: &Info) -> BuiltRegex { 288 | let mut regex_string = r"(?<=^|\W)(".to_string(); 289 | for (i, (key, _)) in info.map.iter().enumerate() { 290 | if i > 0 { 291 | regex_string += "|"; 292 | } 293 | 294 | regex_string += &escape(key); 295 | } 296 | regex_string += r")(?=$|\W)"; 297 | 298 | let regex = Regex::new(®ex_string).unwrap(); 299 | BuiltRegex(regex) 300 | } 301 | 302 | pub fn convert(regex: BuiltRegex, mut s: String, info: &Info) -> String { 303 | let s_cloned = s.clone(); 304 | // find names that are in the map, and replace them with an url. 305 | let captures = regex.0.captures_iter(s_cloned.as_bytes()); 306 | 307 | let mut delta = 0i64; 308 | for cap in captures { 309 | let m = cap.unwrap().get(1).unwrap(); 310 | // safety: we know the input bytes to the regex is well-formed utf-8, the regex only 311 | // works on utf-8 runes. Therefore, the capture group also must only contain 312 | // well-formed utf-8 bytes. 313 | let name = unsafe { std::str::from_utf8_unchecked(m.as_bytes()) }; 314 | let to = info.map.get(name).unwrap(); 315 | let to = to.to_url_string(); 316 | let to = format!("{}", to, name); 317 | 318 | let start = (m.start() as i64 + delta) as usize; 319 | let end = (m.end() as i64 + delta) as usize; 320 | 321 | s.replace_range(start..end, &to); 322 | 323 | delta += (to.len() as i64) - ((end - start) as i64); 324 | } 325 | 326 | s 327 | } 328 | 329 | #[cfg(test)] 330 | mod tests { 331 | use std::collections::HashMap; 332 | 333 | use ruma::identifiers::user_id; 334 | 335 | use crate::convert::to_matrix::{build_regex, convert, Info}; 336 | use crate::MatrixToItem; 337 | 338 | #[test] 339 | fn test_mapping() { 340 | let before = "hello tom"; 341 | let after = "hello tom"; 342 | 343 | let mut map = HashMap::new(); 344 | let sed = user_id!("@tomsg_tom:lieuwe.xyz"); 345 | map.insert("tom".to_string(), MatrixToItem::User(&sed)); 346 | 347 | let info = Info { map }; 348 | let regex = build_regex(&info); 349 | 350 | assert_eq!(after, convert(regex, before.to_string(), &info)); 351 | } 352 | 353 | #[test] 354 | fn test_mapping2() { 355 | let before = "hello sed[m]"; 356 | let after = "hello sed[m]"; 357 | 358 | let mut map = HashMap::new(); 359 | let sed = user_id!("@sed:t2bot.io"); 360 | map.insert("sed[m]".to_string(), MatrixToItem::User(&sed)); 361 | 362 | let info = Info { map }; 363 | let regex = build_regex(&info); 364 | 365 | assert_eq!(after, convert(regex, before.to_string(), &info)); 366 | } 367 | 368 | #[test] 369 | fn test_mapping_double() { 370 | let before = "hello sed[m] voyager[m]"; 371 | let after = "hello sed[m] voyager[m]"; 372 | 373 | let mut map = HashMap::new(); 374 | let sed = user_id!("@sed:t2bot.io"); 375 | map.insert("sed[m]".to_string(), MatrixToItem::User(&sed)); 376 | let voyager = user_id!("@voyager:t2bot.io"); 377 | map.insert("voyager[m]".to_string(), MatrixToItem::User(&voyager)); 378 | 379 | let info = Info { map }; 380 | let regex = build_regex(&info); 381 | 382 | assert_eq!(after, convert(regex, before.to_string(), &info)); 383 | } 384 | } 385 | } 386 | 387 | /* 388 | use scraper::{Html, NodeMut, Selector}; 389 | 390 | fn clean_matrix_tree(node: &mut NodeMut) { 391 | if !node.has_children() { 392 | return; 393 | } 394 | 395 | // lets get the children 396 | let mut vec = vec![]; 397 | 398 | let mut child = node.first_child(); 399 | loop { 400 | let child = match child { 401 | Some(c) => c, 402 | None => break, 403 | }; 404 | 405 | vec.push(child); 406 | 407 | child = child.next_sibling(); 408 | } 409 | 410 | // now, with the children, do magic. 411 | 412 | for child in vec { 413 | let node = child.value(); 414 | match node { 415 | Element(el) => { 416 | match el.name() { 417 | _ => el 418 | } 419 | } 420 | _ => {} 421 | } 422 | } 423 | } 424 | 425 | pub fn to_tomsg(s: &str) -> String { 426 | let mut frag = Html::parse_fragment(s); 427 | for child in frag.tree.root_mut().children() {} 428 | } 429 | */ 430 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod appservice; 2 | mod mappingdict; 3 | mod matrix; 4 | mod request; 5 | mod util; 6 | 7 | #[cfg(feature = "convert")] 8 | pub mod convert; 9 | 10 | pub use appservice::*; 11 | pub use mappingdict::*; 12 | pub use matrix::*; 13 | pub use request::RequestBuilder; 14 | 15 | #[cfg(feature = "serve")] 16 | mod server; 17 | #[cfg(feature = "serve")] 18 | pub use server::serve; 19 | -------------------------------------------------------------------------------- /src/mappingdict.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::collections::HashMap; 3 | use std::hash::Hash; 4 | 5 | /// An ID being either a Matrix ID or an external ID for one object. 6 | #[derive(Debug, PartialEq, Eq, Hash)] 7 | pub enum MappingId<'a, E: ?Sized, M: ?Sized> { 8 | /// A reference to the ID of an external object. 9 | External(&'a E), 10 | /// A refernece to the ID of a Matrix object. 11 | Matrix(&'a M), 12 | } 13 | 14 | impl<'a, E: ?Sized, M: ?Sized> Clone for MappingId<'a, E, M> { 15 | fn clone(&self) -> Self { 16 | match self { 17 | Self::External(e) => Self::External(e), 18 | Self::Matrix(m) => Self::Matrix(m), 19 | } 20 | } 21 | } 22 | 23 | /// Represents an object that has both a Matrix ID and an external ID. 24 | pub trait Mappable { 25 | type MatrixReference: ?Sized + Eq + Hash + ToOwned; 26 | type MatrixType: Eq + Hash + Borrow; 27 | type ExternalReference: ?Sized + Eq + Hash + ToOwned; 28 | type ExternalType: Eq + Hash + Borrow; 29 | 30 | /// Get a reference to the Matrix ID of this object. 31 | fn as_matrix(&self) -> &Self::MatrixReference; 32 | /// Convert this object into an owned Matrix ID of this object. 33 | fn into_matrix(self) -> Self::MatrixType; 34 | /// Get a reference to the external ID of this object. 35 | fn as_external(&self) -> &Self::ExternalReference; 36 | /// Convert this object into an owned external ID of this object. 37 | fn into_external(self) -> Self::ExternalType; 38 | 39 | /// Split this object into owned matrix type and external type. 40 | fn into_split(self) -> (Self::MatrixType, Self::ExternalType); 41 | } 42 | 43 | /// A map comparable to a `HashMap` which contains items that are `Mappable`. 44 | /// The map keeps track of the mapping between both the external type and Matrix type and an 45 | /// object. 46 | #[derive(Debug, Clone)] 47 | pub struct MappingDict { 48 | items: Vec, 49 | external_to_index: HashMap, 50 | matrix_to_index: HashMap, 51 | } 52 | 53 | impl MappingDict 54 | where 55 | V: Mappable, 56 | { 57 | /// Create a new empty `MappingDict`. 58 | pub fn new() -> Self { 59 | Self { 60 | items: vec![], 61 | external_to_index: HashMap::new(), 62 | matrix_to_index: HashMap::new(), 63 | } 64 | } 65 | 66 | /// Create a new `MappingDict` consuming the given `Vec` of items. 67 | /// All items are put into the newly created map. 68 | /// 69 | /// This is more efficient than just calling `insert` yourself on an empty map, since this 70 | /// method will initialize the vector and hashmap with a starting capacpity, thus resulting in 71 | /// less allocations. 72 | pub fn from_vec(items: Vec) -> Self { 73 | let mut res = Self { 74 | items: Vec::with_capacity(items.len()), 75 | matrix_to_index: HashMap::with_capacity(items.len()), 76 | external_to_index: HashMap::with_capacity(items.len()), 77 | }; 78 | 79 | for item in items { 80 | res.insert(item); 81 | } 82 | 83 | res 84 | } 85 | 86 | /// Inserts the given `item` in the current `MappingDict`. 87 | /// Allocates if neccesary. 88 | /// 89 | /// Returns a mutable reference to the newly inserted item. 90 | pub fn insert(&mut self, item: V) -> &mut V { 91 | let index = self.items.len(); 92 | 93 | self.matrix_to_index 94 | .insert(item.as_matrix().to_owned(), index); 95 | self.external_to_index 96 | .insert(item.as_external().to_owned(), index); 97 | self.items.push(item); 98 | 99 | &mut self.items[index] 100 | } 101 | 102 | /// Returns a reference to the item associated with the given `identifier`, or `None` if no 103 | /// such item exists. 104 | pub fn get( 105 | &self, 106 | identifier: MappingId, 107 | ) -> Option<&V> { 108 | let index = match identifier { 109 | MappingId::Matrix(m) => self.matrix_to_index.get(m), 110 | MappingId::External(e) => self.external_to_index.get(e), 111 | }; 112 | 113 | match index { 114 | None => None, 115 | Some(i) => self.items.get(*i), 116 | } 117 | } 118 | 119 | /// Returns a mutable reference to the item associated with the given `identifier`, or `None` 120 | /// if no such item exists. 121 | pub fn get_mut( 122 | &mut self, 123 | identifier: MappingId, 124 | ) -> Option<&mut V> { 125 | let index = match identifier { 126 | MappingId::Matrix(m) => self.matrix_to_index.get(m), 127 | MappingId::External(e) => self.external_to_index.get(e), 128 | }; 129 | 130 | match index { 131 | None => None, 132 | Some(i) => self.items.get_mut(*i), 133 | } 134 | } 135 | 136 | /// Returns whether or not this `MappingDict` contains an item associated with the given 137 | /// `identifier`. 138 | pub fn has(&self, identifier: MappingId) -> bool { 139 | match identifier { 140 | MappingId::Matrix(m) => self.matrix_to_index.contains_key(m), 141 | MappingId::External(e) => self.external_to_index.contains_key(e), 142 | } 143 | } 144 | 145 | /// If this `MappingDict` contains an item associated with the given `identifier`, remove it 146 | /// and return the value that was contained in the `MappingDict`. 147 | /// If no such item exists, this function returns `None`. 148 | pub fn remove( 149 | &mut self, 150 | identifier: MappingId, 151 | ) -> Option { 152 | let index = match identifier { 153 | MappingId::Matrix(m) => self.matrix_to_index.remove(m), 154 | MappingId::External(e) => self.external_to_index.remove(e), 155 | }; 156 | 157 | if let Some(id) = index { 158 | let item = self.items.remove(id); 159 | 160 | match identifier { 161 | MappingId::Matrix(_) => self.external_to_index.remove(item.as_external()), 162 | MappingId::External(_) => self.matrix_to_index.remove(item.as_matrix()), 163 | }; 164 | 165 | Some(item) 166 | } else { 167 | None 168 | } 169 | } 170 | 171 | /// Get an iterator over references of the items contained in this `MappingDict`. 172 | pub fn iter(&'_ self) -> std::slice::Iter<'_, V> { 173 | self.items.iter() 174 | } 175 | 176 | /// Get an iterator over mutable references of the items contained in this `MappingDict`. 177 | pub fn iter_mut(&'_ mut self) -> std::slice::IterMut<'_, V> { 178 | self.items.iter_mut() 179 | } 180 | 181 | /// Shrinks the capacity of the map as much as possible. It will drop down as much as possible 182 | /// while maintaining the internal rules and possibly leaving some space in accordance with the 183 | /// resize policy. 184 | pub fn shrink_to_fit(&mut self) { 185 | self.items.shrink_to_fit(); 186 | self.matrix_to_index.shrink_to_fit(); 187 | self.external_to_index.shrink_to_fit(); 188 | } 189 | } 190 | 191 | impl Default for MappingDict { 192 | fn default() -> Self { 193 | Self::new() 194 | } 195 | } 196 | 197 | impl<'a, T> IntoIterator for &'a MappingDict 198 | where 199 | T: Mappable, 200 | { 201 | type Item = &'a T; 202 | type IntoIter = std::slice::Iter<'a, T>; 203 | 204 | fn into_iter(self) -> Self::IntoIter { 205 | self.items.iter() 206 | } 207 | } 208 | 209 | impl IntoIterator for MappingDict 210 | where 211 | V: Mappable, 212 | { 213 | type Item = V; 214 | type IntoIter = std::vec::IntoIter; 215 | 216 | fn into_iter(self) -> Self::IntoIter { 217 | self.items.into_iter() 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/matrix.rs: -------------------------------------------------------------------------------- 1 | use ruma::api::exports::http::uri; 2 | use ruma::identifiers::{EventId, MxcUri, RoomId, UserId}; 3 | 4 | /// An item that can be represented using a matrix.to URL. 5 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 6 | pub enum MatrixToItem<'a> { 7 | /// An event, since event IDs are room local a RoomId is required. 8 | Event(&'a RoomId, &'a EventId), 9 | /// An ID of an user. 10 | User(&'a UserId), 11 | /// A ID to a group, the first character must be an +. 12 | Group(&'a String), 13 | } 14 | 15 | impl<'a> MatrixToItem<'a> { 16 | /// Convert the current `MatrixToItem` into a `String`. 17 | pub fn to_url_string(&self) -> String { 18 | let slug = match self { 19 | MatrixToItem::Event(room_id, event_id) => format!("{}/{}", room_id, event_id), 20 | MatrixToItem::User(user_id) => user_id.to_string(), 21 | MatrixToItem::Group(group_id) => group_id.to_string(), 22 | }; 23 | 24 | format!("https://matrix.to/#/{}", slug) 25 | } 26 | } 27 | 28 | /// An error from converting an MXC URI to a HTTP URL. 29 | #[derive(Debug)] 30 | pub enum MxcConversionError { 31 | /// The given MXC URI is malformed. 32 | InvalidMxc, 33 | /// There was an error parsing the resulting URL into an URI object. 34 | UriParseError(uri::InvalidUri), 35 | } 36 | 37 | impl From for MxcConversionError { 38 | fn from(err: uri::InvalidUri) -> Self { 39 | MxcConversionError::UriParseError(err) 40 | } 41 | } 42 | 43 | /// Convert the given MXC URI into a HTTP URL, using the given `homeserver_url` as the host to the 44 | /// MXC content. 45 | pub fn mxc_to_url( 46 | homeserver_url: &uri::Uri, 47 | mxc_uri: &MxcUri, 48 | ) -> Result { 49 | let (server_name, id) = mxc_uri.parts().ok_or(MxcConversionError::InvalidMxc)?; 50 | 51 | let res = format!( 52 | "{}_matrix/media/r0/download/{}/{}", 53 | homeserver_url, server_name, id 54 | ); 55 | Ok(res.parse()?) 56 | } 57 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use ruma::identifiers::UserId; 4 | use ruma_client::{Client, HttpClient, ResponseResult}; 5 | 6 | use hyper::Uri; 7 | 8 | /// A builder for a request to the Matrix homeserver. 9 | #[derive(Debug, Clone)] 10 | pub struct RequestBuilder<'a, C, R> 11 | where 12 | C: HttpClient, 13 | R: ruma::api::OutgoingRequest, 14 | { 15 | client: &'a Client, 16 | request: R, 17 | 18 | params: HashMap, 19 | } 20 | 21 | impl<'a, C, R> RequestBuilder<'a, C, R> 22 | where 23 | C: HttpClient, 24 | R: ruma::api::OutgoingRequest, 25 | { 26 | /// Create a new `RequestBuilder`, with the given `Client` and the given `request`. 27 | pub fn new(client: &'a Client, request: R) -> Self { 28 | Self { 29 | client, 30 | request, 31 | 32 | params: HashMap::new(), 33 | } 34 | } 35 | 36 | /// Set the `user_id` url parameter, returning the current builder to allow method chaining. 37 | pub fn user_id(&mut self, user_id: &UserId) -> &mut Self { 38 | self.params 39 | .insert(String::from("user_id"), user_id.to_string()); 40 | self 41 | } 42 | 43 | /// Set the `ts` url parameter, returning the current builder to allow method chaining. 44 | pub fn timestamp(&mut self, timestamp: i64) -> &mut Self { 45 | self.params 46 | .insert(String::from("ts"), timestamp.to_string()); 47 | self 48 | } 49 | 50 | /// Set the `access_token` url parameter, returning the current builder to allow method 51 | /// chaining. 52 | pub fn access_token(&mut self, access_token: String) -> &mut Self { 53 | self.params 54 | .insert(String::from("access_token"), access_token); 55 | self 56 | } 57 | 58 | /// Submit the request, waiting on the response. 59 | /// This will consume the current builder. 60 | pub async fn request(self) -> ResponseResult { 61 | let mut new_params = String::new(); 62 | for (i, s) in self 63 | .params 64 | .into_iter() 65 | .map(|(k, v)| format!("{}={}", k, v)) 66 | .enumerate() 67 | { 68 | if i > 0 { 69 | new_params.push('&'); 70 | } 71 | new_params.push_str(&s); 72 | } 73 | 74 | self.client 75 | .send_customized_request(self.request, |req| { 76 | let uri = req.uri_mut(); 77 | let new_path_and_query = match uri.query() { 78 | Some(params) => format!("{}?{}&{}", uri.path(), params, new_params), 79 | None => format!("{}?{}", uri.path(), new_params), 80 | }; 81 | 82 | let mut parts = uri.clone().into_parts(); 83 | parts.path_and_query = Some(new_path_and_query.parse()?); 84 | *uri = Uri::from_parts(parts)?; 85 | 86 | Ok(()) 87 | }) 88 | .await 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::future::Future; 3 | use std::net::ToSocketAddrs; 4 | 5 | use ruma::events::AnyRoomEvent; 6 | use ruma::serde::Raw; 7 | 8 | use hyper::server::Server; 9 | use hyper::service::{make_service_fn, service_fn}; 10 | use hyper::{ 11 | body::{aggregate, Buf}, 12 | header, Body, Request, Response, StatusCode, 13 | }; 14 | 15 | use serde_json::value::to_raw_value; 16 | 17 | /// Listen on `addrs` for incoming events, and use the given `handler` to handle those events. 18 | pub async fn serve(addrs: S, handler: F) -> Result<(), hyper::Error> 19 | where 20 | S: ToSocketAddrs, 21 | F: Fn(String, Vec>) -> R + Sync + Send + Clone + 'static, 22 | R: Future> + Send, 23 | { 24 | let service = make_service_fn(move |_| { 25 | let handler = handler.clone(); 26 | async { 27 | let f = service_fn(move |req: Request| { 28 | let handler = handler.clone(); 29 | async move { 30 | let (parts, body) = req.into_parts(); 31 | 32 | // skip "/transactions/" 33 | let txn_id = parts.uri.path()[14..].to_string(); 34 | 35 | let body = aggregate(body).await.unwrap(); 36 | let json: serde_json::Value = serde_json::from_reader(body.reader()).unwrap(); 37 | 38 | let events: Vec> = json 39 | .as_object() 40 | .unwrap() 41 | .get("events") 42 | .unwrap() 43 | .as_array() 44 | .unwrap() 45 | .iter() 46 | .map(|e| { 47 | let raw_value = to_raw_value(e).unwrap(); 48 | Raw::from_json(raw_value) 49 | }) 50 | .collect(); 51 | 52 | match handler(txn_id, events).await { 53 | Err(_) => {} // TODO 54 | Ok(_) => {} 55 | } 56 | 57 | let response = Response::builder() 58 | .status(StatusCode::OK) 59 | .header(header::CONTENT_TYPE, "application/json") 60 | .body(Body::from("{}")) 61 | .unwrap(); 62 | Ok::<_, Infallible>(response) 63 | } 64 | }); 65 | 66 | Ok::<_, Infallible>(f) 67 | } 68 | }); 69 | 70 | let addr = addrs.to_socket_addrs().unwrap().next().unwrap(); 71 | let server = Server::bind(&addr).serve(service); 72 | 73 | server.await 74 | } 75 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | /// Generate a `String` of length `n_chars` consisting of cryptographically random alphanumeric 2 | /// characters. 3 | #[cfg(feature = "rand")] 4 | pub fn random_alphanumeric(n_chars: usize) -> String { 5 | use rand::{distributions::Alphanumeric, thread_rng, Rng}; 6 | thread_rng() 7 | .sample_iter(Alphanumeric) 8 | .take(n_chars) 9 | .map(char::from) 10 | .collect() 11 | } 12 | --------------------------------------------------------------------------------