Merge pull request 'More Federation' (#181) from docs into master
Reviewed-on: https://git.koesters.xyz/timo/conduit/pulls/181
This commit is contained in:
		
						commit
						8e55623bde
					
				
					 22 changed files with 974 additions and 308 deletions
				
			
		
							
								
								
									
										27
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										27
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							|  | @ -267,6 +267,7 @@ checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" | ||||||
| name = "conduit" | name = "conduit" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  |  "base64 0.12.3", | ||||||
|  "directories", |  "directories", | ||||||
|  "http", |  "http", | ||||||
|  "image", |  "image", | ||||||
|  | @ -1559,7 +1560,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma" | name = "ruma" | ||||||
| version = "0.0.1" | version = "0.0.1" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "ruma-api", |  "ruma-api", | ||||||
|  "ruma-client-api", |  "ruma-client-api", | ||||||
|  | @ -1573,7 +1574,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-api" | name = "ruma-api" | ||||||
| version = "0.17.0-alpha.1" | version = "0.17.0-alpha.1" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "http", |  "http", | ||||||
|  "percent-encoding", |  "percent-encoding", | ||||||
|  | @ -1588,7 +1589,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-api-macros" | name = "ruma-api-macros" | ||||||
| version = "0.17.0-alpha.1" | version = "0.17.0-alpha.1" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "proc-macro-crate", |  "proc-macro-crate", | ||||||
|  "proc-macro2", |  "proc-macro2", | ||||||
|  | @ -1599,7 +1600,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-client-api" | name = "ruma-client-api" | ||||||
| version = "0.10.0-alpha.1" | version = "0.10.0-alpha.1" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "assign", |  "assign", | ||||||
|  "http", |  "http", | ||||||
|  | @ -1617,7 +1618,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-common" | name = "ruma-common" | ||||||
| version = "0.2.0" | version = "0.2.0" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "js_int", |  "js_int", | ||||||
|  "ruma-identifiers", |  "ruma-identifiers", | ||||||
|  | @ -1630,7 +1631,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-events" | name = "ruma-events" | ||||||
| version = "0.22.0-alpha.1" | version = "0.22.0-alpha.1" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "js_int", |  "js_int", | ||||||
|  "ruma-common", |  "ruma-common", | ||||||
|  | @ -1645,7 +1646,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-events-macros" | name = "ruma-events-macros" | ||||||
| version = "0.22.0-alpha.1" | version = "0.22.0-alpha.1" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "proc-macro-crate", |  "proc-macro-crate", | ||||||
|  "proc-macro2", |  "proc-macro2", | ||||||
|  | @ -1656,7 +1657,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-federation-api" | name = "ruma-federation-api" | ||||||
| version = "0.0.3" | version = "0.0.3" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "js_int", |  "js_int", | ||||||
|  "ruma-api", |  "ruma-api", | ||||||
|  | @ -1671,7 +1672,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-identifiers" | name = "ruma-identifiers" | ||||||
| version = "0.17.4" | version = "0.17.4" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "rand", |  "rand", | ||||||
|  "ruma-identifiers-macros", |  "ruma-identifiers-macros", | ||||||
|  | @ -1683,7 +1684,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-identifiers-macros" | name = "ruma-identifiers-macros" | ||||||
| version = "0.17.4" | version = "0.17.4" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "proc-macro2", |  "proc-macro2", | ||||||
|  "quote", |  "quote", | ||||||
|  | @ -1694,7 +1695,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-identifiers-validation" | name = "ruma-identifiers-validation" | ||||||
| version = "0.1.1" | version = "0.1.1" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "ruma-serde", |  "ruma-serde", | ||||||
|  "serde", |  "serde", | ||||||
|  | @ -1705,7 +1706,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-serde" | name = "ruma-serde" | ||||||
| version = "0.2.3" | version = "0.2.3" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "form_urlencoded", |  "form_urlencoded", | ||||||
|  "itoa", |  "itoa", | ||||||
|  | @ -1717,7 +1718,7 @@ dependencies = [ | ||||||
| [[package]] | [[package]] | ||||||
| name = "ruma-signatures" | name = "ruma-signatures" | ||||||
| version = "0.6.0-dev.1" | version = "0.6.0-dev.1" | ||||||
| source = "git+https://github.com/ruma/ruma?rev=987d48666cf166cf12100b5dbc61b5e3385c4014#987d48666cf166cf12100b5dbc61b5e3385c4014" | source = "git+https://github.com/timokoesters/ruma?branch=timo-fixes#c2adc9ecb85538505ff351dbd883c9106f651744" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "base64 0.12.3", |  "base64 0.12.3", | ||||||
|  "ring", |  "ring", | ||||||
|  |  | ||||||
|  | @ -16,9 +16,10 @@ edition = "2018" | ||||||
| #rocket = { git = "https://github.com/SergioBenitez/Rocket.git", rev = "8d779caa22c63b15a6c3ceb75d8f6d4971b2eb67", features = ["tls"] } # Used to handle requests | #rocket = { git = "https://github.com/SergioBenitez/Rocket.git", rev = "8d779caa22c63b15a6c3ceb75d8f6d4971b2eb67", features = ["tls"] } # Used to handle requests | ||||||
| rocket = { git = "https://github.com/timokoesters/Rocket.git", branch = "empty_parameters", features = ["tls"] } | rocket = { git = "https://github.com/timokoesters/Rocket.git", branch = "empty_parameters", features = ["tls"] } | ||||||
| 
 | 
 | ||||||
| tokio = "0.2.22" # Used for long polling | #ruma = { git = "https://github.com/ruma/ruma", features = ["rand", "client-api", "federation-api", "unstable-pre-spec", "unstable-synapse-quirks"], rev = "987d48666cf166cf12100b5dbc61b5e3385c4014" } # Used for matrix spec type definitions and helpers | ||||||
| ruma = { git = "https://github.com/ruma/ruma", features = ["rand", "client-api", "federation-api", "unstable-pre-spec", "unstable-synapse-quirks"], rev = "987d48666cf166cf12100b5dbc61b5e3385c4014" } # Used for matrix spec type definitions and helpers | ruma = { git = "https://github.com/timokoesters/ruma", features = ["rand", "client-api", "federation-api", "unstable-pre-spec", "unstable-synapse-quirks"], branch = "timo-fixes" } # Used for matrix spec type definitions and helpers | ||||||
| #ruma = { path = "../ruma/ruma", features = ["rand", "client-api", "federation-api", "unstable-pre-spec", "unstable-synapse-quirks"] } | #ruma = { path = "../ruma/ruma", features = ["rand", "client-api", "federation-api", "unstable-pre-spec", "unstable-synapse-quirks"] } | ||||||
|  | tokio = "0.2.22" # Used for long polling | ||||||
| sled = "0.32.0" # Used for storing data permanently | sled = "0.32.0" # Used for storing data permanently | ||||||
| log = "0.4.8" # Used for emitting log entries | log = "0.4.8" # Used for emitting log entries | ||||||
| http = "0.2.1" # Used for rocket<->ruma conversions | http = "0.2.1" # Used for rocket<->ruma conversions | ||||||
|  | @ -31,6 +32,7 @@ rust-argon2 = "0.8.2" # Used to hash passwords | ||||||
| reqwest = "0.10.6" # Used to send requests | reqwest = "0.10.6" # Used to send requests | ||||||
| thiserror = "1.0.19" # Used for conduit::Error type | thiserror = "1.0.19" # Used for conduit::Error type | ||||||
| image = { version = "0.23.4", default-features = false, features = ["jpeg", "png", "gif"] } # Used to generate thumbnails for images | image = { version = "0.23.4", default-features = false, features = ["jpeg", "png", "gif"] } # Used to generate thumbnails for images | ||||||
|  | base64 = "0.12.3" # Used to encode server public key | ||||||
| 
 | 
 | ||||||
| [features] | [features] | ||||||
| default = ["conduit_bin"] | default = ["conduit_bin"] | ||||||
|  |  | ||||||
|  | @ -15,11 +15,18 @@ use ruma::{ | ||||||
|     UserId, |     UserId, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | use register::RegistrationKind; | ||||||
| #[cfg(feature = "conduit_bin")] | #[cfg(feature = "conduit_bin")] | ||||||
| use rocket::{get, post}; | use rocket::{get, post}; | ||||||
| 
 | 
 | ||||||
| const GUEST_NAME_LENGTH: usize = 10; | const GUEST_NAME_LENGTH: usize = 10; | ||||||
| 
 | 
 | ||||||
|  | /// # `GET /_matrix/client/r0/register/available`
 | ||||||
|  | ///
 | ||||||
|  | /// Checks if a username is valid and available on this server.
 | ||||||
|  | ///
 | ||||||
|  | /// - Returns true if no user or appservice on this server claimed this username
 | ||||||
|  | /// - This will not reserve the username, so the username might become invalid when trying to register
 | ||||||
| #[cfg_attr(
 | #[cfg_attr(
 | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     get("/_matrix/client/r0/register/available", data = "<body>") |     get("/_matrix/client/r0/register/available", data = "<body>") | ||||||
|  | @ -53,6 +60,15 @@ pub fn get_register_available_route( | ||||||
|     Ok(get_username_availability::Response { available: true }.into()) |     Ok(get_username_availability::Response { available: true }.into()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// # `POST /_matrix/client/r0/register`
 | ||||||
|  | ///
 | ||||||
|  | /// Register an account on this homeserver.
 | ||||||
|  | ///
 | ||||||
|  | /// - Returns the device id and access_token unless `inhibit_login` is true
 | ||||||
|  | /// - When registering a guest account, all parameters except initial_device_display_name will be
 | ||||||
|  | /// ignored
 | ||||||
|  | /// - Creates a new account and a device for it
 | ||||||
|  | /// - The account will be populated with default account data
 | ||||||
| #[cfg_attr(
 | #[cfg_attr(
 | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     post("/_matrix/client/r0/register", data = "<body>") |     post("/_matrix/client/r0/register", data = "<body>") | ||||||
|  | @ -68,12 +84,24 @@ pub fn register_route( | ||||||
|         )); |         )); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     let is_guest = matches!(body.kind, Some(RegistrationKind::Guest)); | ||||||
|  | 
 | ||||||
|  |     let mut missing_username = false; | ||||||
|  | 
 | ||||||
|     // Validate user id
 |     // Validate user id
 | ||||||
|     let user_id = UserId::parse_with_server_name( |     let user_id = UserId::parse_with_server_name( | ||||||
|         body.username |         if is_guest { | ||||||
|             .clone() |             utils::random_string(GUEST_NAME_LENGTH) | ||||||
|             .unwrap_or_else(|| utils::random_string(GUEST_NAME_LENGTH)) |         } else { | ||||||
|             .to_lowercase(), |             body.username.clone().unwrap_or_else(|| { | ||||||
|  |                 // If the user didn't send a username field, that means the client is just trying
 | ||||||
|  |                 // the get an UIAA error to see available flows
 | ||||||
|  |                 missing_username = true; | ||||||
|  |                 // Just give the user a random name. He won't be able to register with it anyway.
 | ||||||
|  |                 utils::random_string(GUEST_NAME_LENGTH) | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |         .to_lowercase(), | ||||||
|         db.globals.server_name(), |         db.globals.server_name(), | ||||||
|     ) |     ) | ||||||
|     .ok() |     .ok() | ||||||
|  | @ -84,7 +112,7 @@ pub fn register_route( | ||||||
|     ))?; |     ))?; | ||||||
| 
 | 
 | ||||||
|     // Check if username is creative enough
 |     // Check if username is creative enough
 | ||||||
|     if db.users.exists(&user_id)? { |     if !missing_username && db.users.exists(&user_id)? { | ||||||
|         return Err(Error::BadRequest( |         return Err(Error::BadRequest( | ||||||
|             ErrorKind::UserInUse, |             ErrorKind::UserInUse, | ||||||
|             "Desired user ID is already taken.", |             "Desired user ID is already taken.", | ||||||
|  | @ -116,7 +144,19 @@ pub fn register_route( | ||||||
|         return Err(Error::Uiaa(uiaainfo)); |         return Err(Error::Uiaa(uiaainfo)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let password = body.password.clone().unwrap_or_default(); |     if missing_username { | ||||||
|  |         return Err(Error::BadRequest( | ||||||
|  |             ErrorKind::MissingParam, | ||||||
|  |             "Missing username field.", | ||||||
|  |         )); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let password = if is_guest { | ||||||
|  |         None | ||||||
|  |     } else { | ||||||
|  |         body.password.clone() | ||||||
|  |     } | ||||||
|  |     .unwrap_or_default(); | ||||||
| 
 | 
 | ||||||
|     // Create user
 |     // Create user
 | ||||||
|     db.users.create(&user_id, &password)?; |     db.users.create(&user_id, &password)?; | ||||||
|  | @ -134,7 +174,7 @@ pub fn register_route( | ||||||
|         &db.globals, |         &db.globals, | ||||||
|     )?; |     )?; | ||||||
| 
 | 
 | ||||||
|     if body.inhibit_login { |     if !is_guest && body.inhibit_login { | ||||||
|         return Ok(register::Response { |         return Ok(register::Response { | ||||||
|             access_token: None, |             access_token: None, | ||||||
|             user_id, |             user_id, | ||||||
|  | @ -144,10 +184,12 @@ pub fn register_route( | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Generate new device id if the user didn't specify one
 |     // Generate new device id if the user didn't specify one
 | ||||||
|     let device_id = body |     let device_id = if is_guest { | ||||||
|         .device_id |         None | ||||||
|         .clone() |     } else { | ||||||
|         .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); |         body.device_id.clone() | ||||||
|  |     } | ||||||
|  |     .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); | ||||||
| 
 | 
 | ||||||
|     // Generate new token for the device
 |     // Generate new token for the device
 | ||||||
|     let token = utils::random_string(TOKEN_LENGTH); |     let token = utils::random_string(TOKEN_LENGTH); | ||||||
|  | @ -168,6 +210,13 @@ pub fn register_route( | ||||||
|     .into()) |     .into()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// # `POST /_matrix/client/r0/account/password`
 | ||||||
|  | ///
 | ||||||
|  | /// Changes the password of this account.
 | ||||||
|  | ///
 | ||||||
|  | /// - Invalidates all other access tokens if logout_devices is true
 | ||||||
|  | /// - Deletes all other devices and most of their data (to-device events, last seen, etc.) if
 | ||||||
|  | /// logout_devices is true
 | ||||||
| #[cfg_attr(
 | #[cfg_attr(
 | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     post("/_matrix/client/r0/account/password", data = "<body>") |     post("/_matrix/client/r0/account/password", data = "<body>") | ||||||
|  | @ -225,6 +274,11 @@ pub fn change_password_route( | ||||||
|     Ok(change_password::Response.into()) |     Ok(change_password::Response.into()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// # `GET _matrix/client/r0/account/whoami`
 | ||||||
|  | ///
 | ||||||
|  | /// Get user_id of this account.
 | ||||||
|  | ///
 | ||||||
|  | /// - Also works for Application Services
 | ||||||
| #[cfg_attr(
 | #[cfg_attr(
 | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     get("/_matrix/client/r0/account/whoami", data = "<body>") |     get("/_matrix/client/r0/account/whoami", data = "<body>") | ||||||
|  | @ -237,6 +291,14 @@ pub fn whoami_route(body: Ruma<whoami::Request>) -> ConduitResult<whoami::Respon | ||||||
|     .into()) |     .into()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// # `POST /_matrix/client/r0/account/deactivate`
 | ||||||
|  | ///
 | ||||||
|  | /// Deactivate this user's account
 | ||||||
|  | ///
 | ||||||
|  | /// - Leaves all rooms and rejects all invitations
 | ||||||
|  | /// - Invalidates all access tokens
 | ||||||
|  | /// - Deletes all devices
 | ||||||
|  | /// - Removes ability to log in again
 | ||||||
| #[cfg_attr(
 | #[cfg_attr(
 | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     post("/_matrix/client/r0/account/deactivate", data = "<body>") |     post("/_matrix/client/r0/account/deactivate", data = "<body>") | ||||||
|  |  | ||||||
|  | @ -1,8 +1,11 @@ | ||||||
| use super::State; | use super::State; | ||||||
| use crate::{ConduitResult, Database, Error, Ruma}; | use crate::{server_server, ConduitResult, Database, Error, Ruma}; | ||||||
| use ruma::api::client::{ | use ruma::api::{ | ||||||
|     error::ErrorKind, |     client::{ | ||||||
|     r0::alias::{create_alias, delete_alias, get_alias}, |         error::ErrorKind, | ||||||
|  |         r0::alias::{create_alias, delete_alias, get_alias}, | ||||||
|  |     }, | ||||||
|  |     federation, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #[cfg(feature = "conduit_bin")] | #[cfg(feature = "conduit_bin")] | ||||||
|  | @ -43,12 +46,25 @@ pub fn delete_alias_route( | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     get("/_matrix/client/r0/directory/room/<_>", data = "<body>") |     get("/_matrix/client/r0/directory/room/<_>", data = "<body>") | ||||||
| )] | )] | ||||||
| pub fn get_alias_route( | pub async fn get_alias_route( | ||||||
|     db: State<'_, Database>, |     db: State<'_, Database>, | ||||||
|     body: Ruma<get_alias::IncomingRequest>, |     body: Ruma<get_alias::IncomingRequest>, | ||||||
| ) -> ConduitResult<get_alias::Response> { | ) -> ConduitResult<get_alias::Response> { | ||||||
|     if body.room_alias.server_name() != db.globals.server_name() { |     if body.room_alias.server_name() != db.globals.server_name() { | ||||||
|         todo!("ask remote server"); |         let response = server_server::send_request( | ||||||
|  |             &db, | ||||||
|  |             body.room_alias.server_name().to_string(), | ||||||
|  |             federation::query::get_room_information::v1::Request { | ||||||
|  |                 room_alias: body.room_alias.to_string(), | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         .await?; | ||||||
|  | 
 | ||||||
|  |         return Ok(get_alias::Response { | ||||||
|  |             room_id: response.room_id, | ||||||
|  |             servers: response.servers, | ||||||
|  |         } | ||||||
|  |         .into()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let room_id = db |     let room_id = db | ||||||
|  |  | ||||||
|  | @ -5,6 +5,9 @@ use std::collections::BTreeMap; | ||||||
| #[cfg(feature = "conduit_bin")] | #[cfg(feature = "conduit_bin")] | ||||||
| use rocket::get; | use rocket::get; | ||||||
| 
 | 
 | ||||||
|  | /// # `GET /_matrix/client/r0/capabilities`
 | ||||||
|  | ///
 | ||||||
|  | /// Get information on this server's supported feature set and other relevent capabilities.
 | ||||||
| #[cfg_attr(feature = "conduit_bin", get("/_matrix/client/r0/capabilities"))] | #[cfg_attr(feature = "conduit_bin", get("/_matrix/client/r0/capabilities"))] | ||||||
| pub fn get_capabilities_route() -> ConduitResult<get_capabilities::Response> { | pub fn get_capabilities_route() -> ConduitResult<get_capabilities::Response> { | ||||||
|     let mut available = BTreeMap::new(); |     let mut available = BTreeMap::new(); | ||||||
|  |  | ||||||
|  | @ -1,15 +1,18 @@ | ||||||
| use super::State; | use super::State; | ||||||
| use crate::{ConduitResult, Database, Error, Result, Ruma}; | use crate::{server_server, ConduitResult, Database, Error, Result, Ruma}; | ||||||
| use ruma::{ | use ruma::{ | ||||||
|     api::client::{ |     api::{ | ||||||
|         error::ErrorKind, |         client::{ | ||||||
|         r0::{ |             error::ErrorKind, | ||||||
|             directory::{ |             r0::{ | ||||||
|                 self, get_public_rooms, get_public_rooms_filtered, get_room_visibility, |                 directory::{ | ||||||
|                 set_room_visibility, |                     self, get_public_rooms, get_public_rooms_filtered, get_room_visibility, | ||||||
|  |                     set_room_visibility, | ||||||
|  |                 }, | ||||||
|  |                 room, | ||||||
|             }, |             }, | ||||||
|             room, |  | ||||||
|         }, |         }, | ||||||
|  |         federation, | ||||||
|     }, |     }, | ||||||
|     events::{ |     events::{ | ||||||
|         room::{avatar, canonical_alias, guest_access, history_visibility, name, topic}, |         room::{avatar, canonical_alias, guest_access, history_visibility, name, topic}, | ||||||
|  | @ -29,6 +32,46 @@ pub async fn get_public_rooms_filtered_route( | ||||||
|     db: State<'_, Database>, |     db: State<'_, Database>, | ||||||
|     body: Ruma<get_public_rooms_filtered::IncomingRequest>, |     body: Ruma<get_public_rooms_filtered::IncomingRequest>, | ||||||
| ) -> ConduitResult<get_public_rooms_filtered::Response> { | ) -> ConduitResult<get_public_rooms_filtered::Response> { | ||||||
|  |     if let Some(other_server) = body | ||||||
|  |         .server | ||||||
|  |         .clone() | ||||||
|  |         .filter(|server| server != &db.globals.server_name().as_str()) | ||||||
|  |     { | ||||||
|  |         let response = server_server::send_request( | ||||||
|  |             &db, | ||||||
|  |             other_server, | ||||||
|  |             federation::directory::get_public_rooms::v1::Request { | ||||||
|  |                 limit: body.limit, | ||||||
|  |                 since: body.since.clone(), | ||||||
|  |                 room_network: federation::directory::get_public_rooms::v1::RoomNetwork::Matrix, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         .await?; | ||||||
|  | 
 | ||||||
|  |         return Ok(get_public_rooms_filtered::Response { | ||||||
|  |             chunk: response | ||||||
|  |                 .chunk | ||||||
|  |                 .into_iter() | ||||||
|  |                 .map(|c| { | ||||||
|  |                     // Convert ruma::api::federation::directory::get_public_rooms::v1::PublicRoomsChunk
 | ||||||
|  |                     // to ruma::api::client::r0::directory::PublicRoomsChunk
 | ||||||
|  |                     Ok::<_, Error>( | ||||||
|  |                         serde_json::from_str( | ||||||
|  |                             &serde_json::to_string(&c) | ||||||
|  |                                 .expect("PublicRoomsChunk::to_string always works"), | ||||||
|  |                         ) | ||||||
|  |                         .expect("federation and client-server PublicRoomsChunk are the same type"), | ||||||
|  |                     ) | ||||||
|  |                 }) | ||||||
|  |                 .filter_map(|r| r.ok()) | ||||||
|  |                 .collect(), | ||||||
|  |             prev_batch: response.prev_batch, | ||||||
|  |             next_batch: response.next_batch, | ||||||
|  |             total_room_count_estimate: response.total_room_count_estimate, | ||||||
|  |         } | ||||||
|  |         .into()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     let limit = body.limit.map_or(10, u64::from); |     let limit = body.limit.map_or(10, u64::from); | ||||||
|     let mut since = 0_u64; |     let mut since = 0_u64; | ||||||
| 
 | 
 | ||||||
|  | @ -169,26 +212,6 @@ pub async fn get_public_rooms_filtered_route( | ||||||
| 
 | 
 | ||||||
|     all_rooms.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members)); |     all_rooms.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members)); | ||||||
| 
 | 
 | ||||||
|     /* |  | ||||||
|     all_rooms.extend_from_slice( |  | ||||||
|         &server_server::send_request( |  | ||||||
|             &db, |  | ||||||
|             "privacytools.io".to_owned(), |  | ||||||
|             ruma::api::federation::v1::get_public_rooms::Request { |  | ||||||
|                 limit: Some(20_u32.into()), |  | ||||||
|                 since: None, |  | ||||||
|                 room_network: ruma::api::federation::v1::get_public_rooms::RoomNetwork::Matrix, |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|         .await |  | ||||||
|         ? |  | ||||||
|         .chunk |  | ||||||
|         .into_iter() |  | ||||||
|         .map(|c| serde_json::from_str(&serde_json::to_string(&c)?)?) |  | ||||||
|         .collect::<Vec<_>>(), |  | ||||||
|     ); |  | ||||||
|     */ |  | ||||||
| 
 |  | ||||||
|     let total_room_count_estimate = (all_rooms.len() as u32).into(); |     let total_room_count_estimate = (all_rooms.len() as u32).into(); | ||||||
| 
 | 
 | ||||||
|     let chunk = all_rooms |     let chunk = all_rooms | ||||||
|  |  | ||||||
|  | @ -1,16 +1,24 @@ | ||||||
| use super::State; | use super::State; | ||||||
| use crate::{pdu::PduBuilder, ConduitResult, Database, Error, Ruma}; | use crate::{ | ||||||
|  |     client_server, pdu::PduBuilder, server_server, utils, ConduitResult, Database, Error, Ruma, | ||||||
|  | }; | ||||||
| use ruma::{ | use ruma::{ | ||||||
|     api::client::{ |     api::{ | ||||||
|         error::ErrorKind, |         client::{ | ||||||
|         r0::membership::{ |             error::ErrorKind, | ||||||
|             ban_user, forget_room, get_member_events, invite_user, join_room_by_id, |             r0::{ | ||||||
|             join_room_by_id_or_alias, joined_members, joined_rooms, kick_user, leave_room, |                 alias, | ||||||
|             unban_user, |                 membership::{ | ||||||
|  |                     ban_user, forget_room, get_member_events, invite_user, join_room_by_id, | ||||||
|  |                     join_room_by_id_or_alias, joined_members, joined_rooms, kick_user, leave_room, | ||||||
|  |                     unban_user, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |         federation, | ||||||
|     }, |     }, | ||||||
|     events::{room::member, EventType}, |     events::{room::member, EventType}, | ||||||
|     Raw, RoomId, |     EventId, Raw, RoomId, RoomVersionId, | ||||||
| }; | }; | ||||||
| use std::{collections::BTreeMap, convert::TryFrom}; | use std::{collections::BTreeMap, convert::TryFrom}; | ||||||
| 
 | 
 | ||||||
|  | @ -21,13 +29,81 @@ use rocket::{get, post}; | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     post("/_matrix/client/r0/rooms/<_>/join", data = "<body>") |     post("/_matrix/client/r0/rooms/<_>/join", data = "<body>") | ||||||
| )] | )] | ||||||
| pub fn join_room_by_id_route( | pub async fn join_room_by_id_route( | ||||||
|     db: State<'_, Database>, |     db: State<'_, Database>, | ||||||
|     body: Ruma<join_room_by_id::IncomingRequest>, |     body: Ruma<join_room_by_id::IncomingRequest>, | ||||||
| ) -> ConduitResult<join_room_by_id::Response> { | ) -> ConduitResult<join_room_by_id::Response> { | ||||||
|     let sender_id = body.sender_id.as_ref().expect("user is authenticated"); |     let sender_id = body.sender_id.as_ref().expect("user is authenticated"); | ||||||
| 
 | 
 | ||||||
|     // TODO: Ask a remote server if we don't have this room
 |     // Ask a remote server if we don't have this room
 | ||||||
|  |     if !db.rooms.exists(&body.room_id)? && body.room_id.server_name() != db.globals.server_name() { | ||||||
|  |         let make_join_response = server_server::send_request( | ||||||
|  |             &db, | ||||||
|  |             body.room_id.server_name().to_string(), | ||||||
|  |             federation::membership::create_join_event_template::v1::Request { | ||||||
|  |                 room_id: body.room_id.clone(), | ||||||
|  |                 user_id: sender_id.clone(), | ||||||
|  |                 ver: vec![RoomVersionId::Version5, RoomVersionId::Version6], | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         .await?; | ||||||
|  | 
 | ||||||
|  |         let mut join_event_stub_value = | ||||||
|  |             serde_json::from_str::<serde_json::Value>(make_join_response.event.json().get()) | ||||||
|  |                 .map_err(|_| { | ||||||
|  |                     Error::BadServerResponse("Invalid make_join event json received from server.") | ||||||
|  |                 })?; | ||||||
|  | 
 | ||||||
|  |         let join_event_stub = | ||||||
|  |             join_event_stub_value | ||||||
|  |                 .as_object_mut() | ||||||
|  |                 .ok_or(Error::BadServerResponse( | ||||||
|  |                     "Invalid make join event object received from server.", | ||||||
|  |                 ))?; | ||||||
|  | 
 | ||||||
|  |         join_event_stub.insert( | ||||||
|  |             "origin".to_owned(), | ||||||
|  |             db.globals.server_name().to_owned().to_string().into(), | ||||||
|  |         ); | ||||||
|  |         join_event_stub.insert( | ||||||
|  |             "origin_server_ts".to_owned(), | ||||||
|  |             utils::millis_since_unix_epoch().into(), | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         // Generate event id
 | ||||||
|  |         let event_id = EventId::try_from(&*format!( | ||||||
|  |             "${}", | ||||||
|  |             ruma::signatures::reference_hash(&join_event_stub_value) | ||||||
|  |                 .expect("ruma can calculate reference hashes") | ||||||
|  |         )) | ||||||
|  |         .expect("ruma's reference hashes are valid event ids"); | ||||||
|  | 
 | ||||||
|  |         // We don't leave the event id into the pdu because that's only allowed in v1 or v2 rooms
 | ||||||
|  |         let join_event_stub = join_event_stub_value.as_object_mut().unwrap(); | ||||||
|  |         join_event_stub.remove("event_id"); | ||||||
|  | 
 | ||||||
|  |         ruma::signatures::hash_and_sign_event( | ||||||
|  |             db.globals.server_name().as_str(), | ||||||
|  |             db.globals.keypair(), | ||||||
|  |             &mut join_event_stub_value, | ||||||
|  |         ) | ||||||
|  |         .expect("event is valid, we just created it"); | ||||||
|  | 
 | ||||||
|  |         let send_join_response = server_server::send_request( | ||||||
|  |             &db, | ||||||
|  |             body.room_id.server_name().to_string(), | ||||||
|  |             federation::membership::create_join_event::v2::Request { | ||||||
|  |                 room_id: body.room_id.clone(), | ||||||
|  |                 event_id, | ||||||
|  |                 pdu_stub: serde_json::from_value::<Raw<_>>(join_event_stub_value) | ||||||
|  |                     .expect("Raw::from_value always works"), | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         .await?; | ||||||
|  | 
 | ||||||
|  |         dbg!(send_join_response); | ||||||
|  |         todo!("Take send_join_response and 'create' the room using that data"); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     let event = member::MemberEventContent { |     let event = member::MemberEventContent { | ||||||
|         membership: member::MembershipState::Join, |         membership: member::MembershipState::Join, | ||||||
|  | @ -61,16 +137,28 @@ pub fn join_room_by_id_route( | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     post("/_matrix/client/r0/join/<_>", data = "<body>") |     post("/_matrix/client/r0/join/<_>", data = "<body>") | ||||||
| )] | )] | ||||||
| pub fn join_room_by_id_or_alias_route( | pub async fn join_room_by_id_or_alias_route( | ||||||
|     db: State<'_, Database>, |     db: State<'_, Database>, | ||||||
|  |     db2: State<'_, Database>, | ||||||
|     body: Ruma<join_room_by_id_or_alias::Request>, |     body: Ruma<join_room_by_id_or_alias::Request>, | ||||||
| ) -> ConduitResult<join_room_by_id_or_alias::Response> { | ) -> ConduitResult<join_room_by_id_or_alias::Response> { | ||||||
|     let room_id = RoomId::try_from(body.room_id_or_alias.clone()).or_else(|alias| { |     let room_id = match RoomId::try_from(body.room_id_or_alias.clone()) { | ||||||
|         Ok::<_, Error>(db.rooms.id_from_alias(&alias)?.ok_or(Error::BadRequest( |         Ok(room_id) => room_id, | ||||||
|             ErrorKind::NotFound, |         Err(room_alias) => { | ||||||
|             "Room not found (TODO: Federation).", |             client_server::get_alias_route( | ||||||
|         ))?) |                 db, | ||||||
|     })?; |                 Ruma { | ||||||
|  |                     body: alias::get_alias::IncomingRequest { room_alias }, | ||||||
|  |                     sender_id: body.sender_id.clone(), | ||||||
|  |                     device_id: body.device_id.clone(), | ||||||
|  |                     json_body: None, | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |             .await? | ||||||
|  |             .0 | ||||||
|  |             .room_id | ||||||
|  |         } | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     let body = Ruma { |     let body = Ruma { | ||||||
|         sender_id: body.sender_id.clone(), |         sender_id: body.sender_id.clone(), | ||||||
|  | @ -83,7 +171,7 @@ pub fn join_room_by_id_or_alias_route( | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     Ok(join_room_by_id_or_alias::Response { |     Ok(join_room_by_id_or_alias::Response { | ||||||
|         room_id: join_room_by_id_route(db, body)?.0.room_id, |         room_id: join_room_by_id_route(db2, body).await?.0.room_id, | ||||||
|     } |     } | ||||||
|     .into()) |     .into()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ mod push; | ||||||
| mod read_marker; | mod read_marker; | ||||||
| mod redact; | mod redact; | ||||||
| mod room; | mod room; | ||||||
|  | mod search; | ||||||
| mod session; | mod session; | ||||||
| mod state; | mod state; | ||||||
| mod sync; | mod sync; | ||||||
|  | @ -47,6 +48,7 @@ pub use push::*; | ||||||
| pub use read_marker::*; | pub use read_marker::*; | ||||||
| pub use redact::*; | pub use redact::*; | ||||||
| pub use room::*; | pub use room::*; | ||||||
|  | pub use search::*; | ||||||
| pub use session::*; | pub use session::*; | ||||||
| pub use state::*; | pub use state::*; | ||||||
| pub use sync::*; | pub use sync::*; | ||||||
|  |  | ||||||
|  | @ -92,13 +92,6 @@ pub fn create_room_route( | ||||||
|         &db.account_data, |         &db.account_data, | ||||||
|     )?; |     )?; | ||||||
| 
 | 
 | ||||||
|     // Figure out preset. We need it for power levels and preset specific events
 |  | ||||||
|     let visibility = body.visibility.unwrap_or(room::Visibility::Private); |  | ||||||
|     let preset = body.preset.unwrap_or_else(|| match visibility { |  | ||||||
|         room::Visibility::Private => create_room::RoomPreset::PrivateChat, |  | ||||||
|         room::Visibility::Public => create_room::RoomPreset::PublicChat, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // 3. Power levels
 |     // 3. Power levels
 | ||||||
|     let mut users = BTreeMap::new(); |     let mut users = BTreeMap::new(); | ||||||
|     users.insert(sender_id.clone(), 100.into()); |     users.insert(sender_id.clone(), 100.into()); | ||||||
|  | @ -142,6 +135,14 @@ pub fn create_room_route( | ||||||
|     )?; |     )?; | ||||||
| 
 | 
 | ||||||
|     // 4. Events set by preset
 |     // 4. Events set by preset
 | ||||||
|  | 
 | ||||||
|  |     // Figure out preset. We need it for preset specific events
 | ||||||
|  |     let visibility = body.visibility.unwrap_or(room::Visibility::Private); | ||||||
|  |     let preset = body.preset.unwrap_or_else(|| match visibility { | ||||||
|  |         room::Visibility::Private => create_room::RoomPreset::PrivateChat, | ||||||
|  |         room::Visibility::Public => create_room::RoomPreset::PublicChat, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     // 4.1 Join Rules
 |     // 4.1 Join Rules
 | ||||||
|     db.rooms.append_pdu( |     db.rooms.append_pdu( | ||||||
|         PduBuilder { |         PduBuilder { | ||||||
|  |  | ||||||
							
								
								
									
										86
									
								
								src/client_server/search.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/client_server/search.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | ||||||
|  | use super::State; | ||||||
|  | use crate::{ConduitResult, Database, Error, Ruma}; | ||||||
|  | use js_int::uint; | ||||||
|  | use ruma::api::client::{error::ErrorKind, r0::search::search_events}; | ||||||
|  | 
 | ||||||
|  | #[cfg(feature = "conduit_bin")] | ||||||
|  | use rocket::post; | ||||||
|  | use search_events::{ResultCategories, ResultRoomEvents, SearchResult}; | ||||||
|  | use std::collections::BTreeMap; | ||||||
|  | 
 | ||||||
|  | #[cfg_attr(
 | ||||||
|  |     feature = "conduit_bin", | ||||||
|  |     post("/_matrix/client/r0/search", data = "<body>") | ||||||
|  | )] | ||||||
|  | pub fn search_events_route( | ||||||
|  |     db: State<'_, Database>, | ||||||
|  |     body: Ruma<search_events::Request>, | ||||||
|  | ) -> ConduitResult<search_events::Response> { | ||||||
|  |     let sender_id = body.sender_id.as_ref().expect("user is authenticated"); | ||||||
|  | 
 | ||||||
|  |     let search_criteria = body.search_categories.room_events.as_ref().unwrap(); | ||||||
|  |     let filter = search_criteria.filter.as_ref().unwrap(); | ||||||
|  | 
 | ||||||
|  |     let room_id = filter.rooms.as_ref().unwrap().first().unwrap(); | ||||||
|  | 
 | ||||||
|  |     let limit = filter.limit.map_or(10, |l| u64::from(l) as usize); | ||||||
|  | 
 | ||||||
|  |     if !db.rooms.is_joined(sender_id, &room_id)? { | ||||||
|  |         return Err(Error::BadRequest( | ||||||
|  |             ErrorKind::Forbidden, | ||||||
|  |             "You don't have permission to view this room.", | ||||||
|  |         )); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let skip = match body.next_batch.as_ref().map(|s| s.parse()) { | ||||||
|  |         Some(Ok(s)) => s, | ||||||
|  |         Some(Err(_)) => { | ||||||
|  |             return Err(Error::BadRequest( | ||||||
|  |                 ErrorKind::InvalidParam, | ||||||
|  |                 "Invalid next_batch token.", | ||||||
|  |             )) | ||||||
|  |         } | ||||||
|  |         None => 0, // Default to the start
 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let search = db | ||||||
|  |         .rooms | ||||||
|  |         .search_pdus(&room_id, &search_criteria.search_term)?; | ||||||
|  | 
 | ||||||
|  |     let results = search | ||||||
|  |         .0 | ||||||
|  |         .map(|result| { | ||||||
|  |             Ok::<_, Error>(SearchResult { | ||||||
|  |                 context: None, | ||||||
|  |                 rank: None, | ||||||
|  |                 result: db | ||||||
|  |                     .rooms | ||||||
|  |                     .get_pdu_from_id(&result)? | ||||||
|  |                     .map(|pdu| pdu.to_room_event()), | ||||||
|  |             }) | ||||||
|  |         }) | ||||||
|  |         .filter_map(|r| r.ok()) | ||||||
|  |         .skip(skip) | ||||||
|  |         .take(limit) | ||||||
|  |         .collect::<Vec<_>>(); | ||||||
|  | 
 | ||||||
|  |     let next_batch = if results.len() < limit as usize { | ||||||
|  |         None | ||||||
|  |     } else { | ||||||
|  |         Some((skip + limit).to_string()) | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     Ok(search_events::Response { | ||||||
|  |         search_categories: ResultCategories { | ||||||
|  |             room_events: Some(ResultRoomEvents { | ||||||
|  |                 count: uint!(0),         // TODO
 | ||||||
|  |                 groups: BTreeMap::new(), // TODO
 | ||||||
|  |                 next_batch, | ||||||
|  |                 results, | ||||||
|  |                 state: BTreeMap::new(), // TODO
 | ||||||
|  |                 highlights: search.1, | ||||||
|  |             }), | ||||||
|  |         }, | ||||||
|  |     } | ||||||
|  |     .into()) | ||||||
|  | } | ||||||
|  | @ -12,14 +12,28 @@ use ruma::{ | ||||||
| #[cfg(feature = "conduit_bin")] | #[cfg(feature = "conduit_bin")] | ||||||
| use rocket::{get, post}; | use rocket::{get, post}; | ||||||
| 
 | 
 | ||||||
|  | /// # `GET /_matrix/client/r0/login`
 | ||||||
|  | ///
 | ||||||
|  | /// Get the homeserver's supported login types. One of these should be used as the `type` field
 | ||||||
|  | /// when logging in.
 | ||||||
| #[cfg_attr(feature = "conduit_bin", get("/_matrix/client/r0/login"))] | #[cfg_attr(feature = "conduit_bin", get("/_matrix/client/r0/login"))] | ||||||
| pub fn get_login_route() -> ConduitResult<get_login_types::Response> { | pub fn get_login_types_route() -> ConduitResult<get_login_types::Response> { | ||||||
|     Ok(get_login_types::Response { |     Ok(get_login_types::Response { | ||||||
|         flows: vec![get_login_types::LoginType::Password], |         flows: vec![get_login_types::LoginType::Password], | ||||||
|     } |     } | ||||||
|     .into()) |     .into()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// # `POST /_matrix/client/r0/login`
 | ||||||
|  | ///
 | ||||||
|  | /// Authenticates the user and returns an access token it can use in subsequent requests.
 | ||||||
|  | ///
 | ||||||
|  | /// - The returned access token is associated with the user and device
 | ||||||
|  | /// - Old access tokens of that device should be invalidated
 | ||||||
|  | /// - If `device_id` is unknown, a new device will be created
 | ||||||
|  | ///
 | ||||||
|  | /// Note: You can use [`GET /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see
 | ||||||
|  | /// supported login types.
 | ||||||
| #[cfg_attr(
 | #[cfg_attr(
 | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     post("/_matrix/client/r0/login", data = "<body>") |     post("/_matrix/client/r0/login", data = "<body>") | ||||||
|  | @ -74,6 +88,7 @@ pub fn login_route( | ||||||
|     // Generate a new token for the device
 |     // Generate a new token for the device
 | ||||||
|     let token = utils::random_string(TOKEN_LENGTH); |     let token = utils::random_string(TOKEN_LENGTH); | ||||||
| 
 | 
 | ||||||
|  |     // TODO: Don't always create a new device
 | ||||||
|     // Add device
 |     // Add device
 | ||||||
|     db.users.create_device( |     db.users.create_device( | ||||||
|         &user_id, |         &user_id, | ||||||
|  | @ -92,6 +107,12 @@ pub fn login_route( | ||||||
|     .into()) |     .into()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// # `POST /_matrix/client/r0/logout`
 | ||||||
|  | ///
 | ||||||
|  | /// Log out the current device.
 | ||||||
|  | ///
 | ||||||
|  | /// - Invalidates the access token
 | ||||||
|  | /// - Deletes the device and most of it's data (to-device events, last seen, etc.)
 | ||||||
| #[cfg_attr(
 | #[cfg_attr(
 | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     post("/_matrix/client/r0/logout", data = "<body>") |     post("/_matrix/client/r0/logout", data = "<body>") | ||||||
|  | @ -108,6 +129,15 @@ pub fn logout_route( | ||||||
|     Ok(logout::Response.into()) |     Ok(logout::Response.into()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// # `POST /_matrix/client/r0/logout/all`
 | ||||||
|  | ///
 | ||||||
|  | /// Log out all devices of this user.
 | ||||||
|  | ///
 | ||||||
|  | /// - Invalidates all access tokens
 | ||||||
|  | /// - Deletes devices and most of their data (to-device events, last seen, etc.)
 | ||||||
|  | ///
 | ||||||
|  | /// Note: This is equivalent to calling [`GET /_matrix/client/r0/logout`](fn.logout_route.html)
 | ||||||
|  | /// from each device of this user.
 | ||||||
| #[cfg_attr(
 | #[cfg_attr(
 | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     post("/_matrix/client/r0/logout/all", data = "<body>") |     post("/_matrix/client/r0/logout/all", data = "<body>") | ||||||
|  |  | ||||||
|  | @ -2,17 +2,29 @@ use super::State; | ||||||
| use crate::{ConduitResult, Database, Error, Ruma}; | use crate::{ConduitResult, Database, Error, Ruma}; | ||||||
| use ruma::{ | use ruma::{ | ||||||
|     api::client::r0::sync::sync_events, |     api::client::r0::sync::sync_events, | ||||||
|     events::{AnySyncEphemeralRoomEvent, EventType}, |     events::{room::member::MembershipState, AnySyncEphemeralRoomEvent, EventType}, | ||||||
|     Raw, |     Raw, RoomId, UserId, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #[cfg(feature = "conduit_bin")] | #[cfg(feature = "conduit_bin")] | ||||||
| use rocket::{get, tokio}; | use rocket::{get, tokio}; | ||||||
| use std::{ | use std::{ | ||||||
|     collections::{hash_map, BTreeMap, HashMap, HashSet}, |     collections::{hash_map, BTreeMap, HashMap, HashSet}, | ||||||
|  |     convert::TryFrom, | ||||||
|     time::Duration, |     time::Duration, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /// # `GET /_matrix/client/r0/sync`
 | ||||||
|  | ///
 | ||||||
|  | /// Synchronize the client's state with the latest state on the server.
 | ||||||
|  | ///
 | ||||||
|  | /// - This endpoint takes a `since` parameter which should be the `next_batch` value from a
 | ||||||
|  | /// previous request.
 | ||||||
|  | /// - Calling this endpoint without a `since` parameter will return all recent events, the state
 | ||||||
|  | /// of all rooms and more data. This should only be called on the initial login of the device.
 | ||||||
|  | /// - To get incremental updates, you can call this endpoint with a `since` parameter. This will
 | ||||||
|  | /// return all recent events, state updates and more data that happened since the last /sync
 | ||||||
|  | /// request.
 | ||||||
| #[cfg_attr(
 | #[cfg_attr(
 | ||||||
|     feature = "conduit_bin", |     feature = "conduit_bin", | ||||||
|     get("/_matrix/client/r0/sync", data = "<body>") |     get("/_matrix/client/r0/sync", data = "<body>") | ||||||
|  | @ -40,7 +52,9 @@ pub async fn sync_events_route( | ||||||
|         .unwrap_or(0); |         .unwrap_or(0); | ||||||
| 
 | 
 | ||||||
|     let mut presence_updates = HashMap::new(); |     let mut presence_updates = HashMap::new(); | ||||||
|  |     let mut left_encrypted_users = HashSet::new(); // Users that have left any encrypted rooms the sender was in
 | ||||||
|     let mut device_list_updates = HashSet::new(); |     let mut device_list_updates = HashSet::new(); | ||||||
|  |     let mut device_list_left = HashSet::new(); | ||||||
| 
 | 
 | ||||||
|     // Look for device list updates of this account
 |     // Look for device list updates of this account
 | ||||||
|     device_list_updates.extend( |     device_list_updates.extend( | ||||||
|  | @ -67,46 +81,100 @@ pub async fn sync_events_route( | ||||||
|             .rev() |             .rev() | ||||||
|             .collect::<Vec<_>>(); |             .collect::<Vec<_>>(); | ||||||
| 
 | 
 | ||||||
|  |         let send_notification_counts = !timeline_pdus.is_empty(); | ||||||
|  | 
 | ||||||
|         // They /sync response doesn't always return all messages, so we say the output is
 |         // They /sync response doesn't always return all messages, so we say the output is
 | ||||||
|         // limited unless there are events in non_timeline_pdus
 |         // limited unless there are events in non_timeline_pdus
 | ||||||
|         //let mut limited = false;
 |         let mut limited = false; | ||||||
| 
 | 
 | ||||||
|         let mut state_pdus = Vec::new(); |         let mut state_pdus = Vec::new(); | ||||||
|         for pdu in non_timeline_pdus { |         for pdu in non_timeline_pdus { | ||||||
|             if pdu.state_key.is_some() { |             if pdu.state_key.is_some() { | ||||||
|                 state_pdus.push(pdu); |                 state_pdus.push(pdu); | ||||||
|             } |             } | ||||||
|  |             limited = true; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         let encrypted_room = db | ||||||
|  |             .rooms | ||||||
|  |             .room_state_get(&room_id, &EventType::RoomEncryption, "")? | ||||||
|  |             .is_some(); | ||||||
|  | 
 | ||||||
|  |         // TODO: optimize this?
 | ||||||
|         let mut send_member_count = false; |         let mut send_member_count = false; | ||||||
|         let mut joined_since_last_sync = false; |         let mut joined_since_last_sync = false; | ||||||
|         let mut send_notification_counts = false; |         let mut new_encrypted_room = false; | ||||||
|         for pdu in db |         for (state_key, pdu) in db | ||||||
|             .rooms |             .rooms | ||||||
|             .pdus_since(&sender_id, &room_id, since)? |             .pdus_since(&sender_id, &room_id, since)? | ||||||
|  |             .filter_map(|r| r.ok()) | ||||||
|  |             .filter_map(|pdu| Some((pdu.state_key.clone()?, pdu))) | ||||||
|         { |         { | ||||||
|             let pdu = pdu?; |  | ||||||
|             send_notification_counts = true; |  | ||||||
|             if pdu.kind == EventType::RoomMember { |             if pdu.kind == EventType::RoomMember { | ||||||
|                 send_member_count = true; |                 send_member_count = true; | ||||||
|                 if !joined_since_last_sync && pdu.state_key == Some(sender_id.to_string()) { | 
 | ||||||
|                     let content = serde_json::from_value::< |                 let content = serde_json::from_value::< | ||||||
|                         Raw<ruma::events::room::member::MemberEventContent>, |                     Raw<ruma::events::room::member::MemberEventContent>, | ||||||
|                     >(pdu.content.clone()) |                 >(pdu.content.clone()) | ||||||
|                     .expect("Raw::from_value always works") |                 .expect("Raw::from_value always works") | ||||||
|                     .deserialize() |                 .deserialize() | ||||||
|                     .map_err(|_| Error::bad_database("Invalid PDU in database."))?; |                 .map_err(|_| Error::bad_database("Invalid PDU in database."))?; | ||||||
|                     if content.membership == ruma::events::room::member::MembershipState::Join { | 
 | ||||||
|                         joined_since_last_sync = true; |                 if pdu.state_key == Some(sender_id.to_string()) | ||||||
|                         // Both send_member_count and joined_since_last_sync are set. There's
 |                     && content.membership == MembershipState::Join | ||||||
|                         // nothing more to do
 |                 { | ||||||
|                         break; |                     joined_since_last_sync = true; | ||||||
|  |                 } else if encrypted_room && content.membership == MembershipState::Join { | ||||||
|  |                     // A new user joined an encrypted room
 | ||||||
|  |                     let user_id = UserId::try_from(state_key) | ||||||
|  |                         .map_err(|_| Error::bad_database("Invalid UserId in member PDU."))?; | ||||||
|  |                     // Add encryption update if we didn't share an encrypted room already
 | ||||||
|  |                     if !share_encrypted_room(&db, &sender_id, &user_id, &room_id) { | ||||||
|  |                         device_list_updates.insert(user_id); | ||||||
|                     } |                     } | ||||||
|  |                 } else if encrypted_room && content.membership == MembershipState::Leave { | ||||||
|  |                     // Write down users that have left encrypted rooms we are in
 | ||||||
|  |                     left_encrypted_users.insert( | ||||||
|  |                         UserId::try_from(state_key) | ||||||
|  |                             .map_err(|_| Error::bad_database("Invalid UserId in member PDU."))?, | ||||||
|  |                     ); | ||||||
|                 } |                 } | ||||||
|  |             } else if pdu.kind == EventType::RoomEncryption { | ||||||
|  |                 new_encrypted_room = true; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let members = db.rooms.room_state_type(&room_id, &EventType::RoomMember)?; |         if joined_since_last_sync && encrypted_room || new_encrypted_room { | ||||||
|  |             // If the user is in a new encrypted room, give them all joined users
 | ||||||
|  |             device_list_updates.extend( | ||||||
|  |                 db.rooms | ||||||
|  |                     .room_members(&room_id) | ||||||
|  |                     .filter_map(|user_id| { | ||||||
|  |                         Some( | ||||||
|  |                             UserId::try_from(user_id.ok()?.clone()) | ||||||
|  |                                 .map_err(|_| { | ||||||
|  |                                     Error::bad_database("Invalid member event state key in db.") | ||||||
|  |                                 }) | ||||||
|  |                                 .ok()?, | ||||||
|  |                         ) | ||||||
|  |                     }) | ||||||
|  |                     .filter(|user_id| { | ||||||
|  |                         // Don't send key updates from the sender to the sender
 | ||||||
|  |                         sender_id != user_id | ||||||
|  |                     }) | ||||||
|  |                     .filter(|user_id| { | ||||||
|  |                         // Only send keys if the sender doesn't share an encrypted room with the target already
 | ||||||
|  |                         !share_encrypted_room(&db, sender_id, user_id, &room_id) | ||||||
|  |                     }), | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Look for device list updates in this room
 | ||||||
|  |         device_list_updates.extend( | ||||||
|  |             db.users | ||||||
|  |                 .keys_changed(&room_id.to_string(), since, None) | ||||||
|  |                 .filter_map(|r| r.ok()), | ||||||
|  |         ); | ||||||
| 
 | 
 | ||||||
|         let (joined_member_count, invited_member_count, heroes) = if send_member_count { |         let (joined_member_count, invited_member_count, heroes) = if send_member_count { | ||||||
|             let joined_member_count = db.rooms.room_members(&room_id).count(); |             let joined_member_count = db.rooms.room_members(&room_id).count(); | ||||||
|  | @ -133,35 +201,17 @@ pub async fn sync_events_route( | ||||||
|                         .map_err(|_| Error::bad_database("Invalid member event in database."))?; |                         .map_err(|_| Error::bad_database("Invalid member event in database."))?; | ||||||
| 
 | 
 | ||||||
|                         if let Some(state_key) = &pdu.state_key { |                         if let Some(state_key) = &pdu.state_key { | ||||||
|                             let current_content = serde_json::from_value::< |                             let user_id = UserId::try_from(state_key.clone()).map_err(|_| { | ||||||
|                                 Raw<ruma::events::room::member::MemberEventContent>, |                                 Error::bad_database("Invalid UserId in member PDU.") | ||||||
|                             >( |  | ||||||
|                                 members |  | ||||||
|                                     .get(state_key) |  | ||||||
|                                     .ok_or_else(|| { |  | ||||||
|                                         Error::bad_database( |  | ||||||
|                                             "A user that joined once has no member event anymore.", |  | ||||||
|                                         ) |  | ||||||
|                                     })? |  | ||||||
|                                     .content |  | ||||||
|                                     .clone(), |  | ||||||
|                             ) |  | ||||||
|                             .expect("Raw::from_value always works") |  | ||||||
|                             .deserialize() |  | ||||||
|                             .map_err(|_| { |  | ||||||
|                                 Error::bad_database("Invalid member event in database.") |  | ||||||
|                             })?; |                             })?; | ||||||
| 
 | 
 | ||||||
|                             // The membership was and still is invite or join
 |                             // The membership was and still is invite or join
 | ||||||
|                             if matches!( |                             if matches!( | ||||||
|                                 content.membership, |                                 content.membership, | ||||||
|                                 ruma::events::room::member::MembershipState::Join |                                 MembershipState::Join | MembershipState::Invite | ||||||
|                                     | ruma::events::room::member::MembershipState::Invite |                             ) && (db.rooms.is_joined(&user_id, &room_id)? | ||||||
|                             ) && matches!( |                                 || db.rooms.is_invited(&user_id, &room_id)?) | ||||||
|                                 current_content.membership, |                             { | ||||||
|                                 ruma::events::room::member::MembershipState::Join |  | ||||||
|                                     | ruma::events::room::member::MembershipState::Invite |  | ||||||
|                             ) { |  | ||||||
|                                 Ok::<_, Error>(Some(state_key.clone())) |                                 Ok::<_, Error>(Some(state_key.clone())) | ||||||
|                             } else { |                             } else { | ||||||
|                                 Ok(None) |                                 Ok(None) | ||||||
|  | @ -274,7 +324,7 @@ pub async fn sync_events_route( | ||||||
|                 notification_count, |                 notification_count, | ||||||
|             }, |             }, | ||||||
|             timeline: sync_events::Timeline { |             timeline: sync_events::Timeline { | ||||||
|                 limited: joined_since_last_sync, |                 limited: limited || joined_since_last_sync, | ||||||
|                 prev_batch, |                 prev_batch, | ||||||
|                 events: room_events, |                 events: room_events, | ||||||
|             }, |             }, | ||||||
|  | @ -297,13 +347,6 @@ pub async fn sync_events_route( | ||||||
|             joined_rooms.insert(room_id.clone(), joined_room); |             joined_rooms.insert(room_id.clone(), joined_room); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Look for device list updates in this room
 |  | ||||||
|         device_list_updates.extend( |  | ||||||
|             db.users |  | ||||||
|                 .keys_changed(&room_id.to_string(), since, None) |  | ||||||
|                 .filter_map(|r| r.ok()), |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         // Take presence updates from this room
 |         // Take presence updates from this room
 | ||||||
|         for (user_id, presence) in |         for (user_id, presence) in | ||||||
|             db.rooms |             db.rooms | ||||||
|  | @ -348,31 +391,6 @@ pub async fn sync_events_route( | ||||||
|             .map(|pdu| pdu.to_sync_room_event()) |             .map(|pdu| pdu.to_sync_room_event()) | ||||||
|             .collect(); |             .collect(); | ||||||
| 
 | 
 | ||||||
|         // TODO: Only until leave point
 |  | ||||||
|         let mut edus = db |  | ||||||
|             .rooms |  | ||||||
|             .edus |  | ||||||
|             .roomlatests_since(&room_id, since)? |  | ||||||
|             .filter_map(|r| r.ok()) // Filter out buggy events
 |  | ||||||
|             .collect::<Vec<_>>(); |  | ||||||
| 
 |  | ||||||
|         if db |  | ||||||
|             .rooms |  | ||||||
|             .edus |  | ||||||
|             .last_roomactive_update(&room_id, &db.globals)? |  | ||||||
|             > since |  | ||||||
|         { |  | ||||||
|             edus.push( |  | ||||||
|                 serde_json::from_str( |  | ||||||
|                     &serde_json::to_string(&AnySyncEphemeralRoomEvent::Typing( |  | ||||||
|                         db.rooms.edus.roomactives_all(&room_id)?, |  | ||||||
|                     )) |  | ||||||
|                     .expect("event is valid, we just created it"), |  | ||||||
|                 ) |  | ||||||
|                 .expect("event is valid, we just created it"), |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let left_room = sync_events::LeftRoom { |         let left_room = sync_events::LeftRoom { | ||||||
|             account_data: sync_events::AccountData { events: Vec::new() }, |             account_data: sync_events::AccountData { events: Vec::new() }, | ||||||
|             timeline: sync_events::Timeline { |             timeline: sync_events::Timeline { | ||||||
|  | @ -383,6 +401,49 @@ pub async fn sync_events_route( | ||||||
|             state: sync_events::State { events: Vec::new() }, |             state: sync_events::State { events: Vec::new() }, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|  |         let mut left_since_last_sync = false; | ||||||
|  |         for pdu in db.rooms.pdus_since(&sender_id, &room_id, since)? { | ||||||
|  |             let pdu = pdu?; | ||||||
|  |             if pdu.kind == EventType::RoomMember && pdu.state_key == Some(sender_id.to_string()) { | ||||||
|  |                 let content = serde_json::from_value::< | ||||||
|  |                     Raw<ruma::events::room::member::MemberEventContent>, | ||||||
|  |                 >(pdu.content.clone()) | ||||||
|  |                 .expect("Raw::from_value always works") | ||||||
|  |                 .deserialize() | ||||||
|  |                 .map_err(|_| Error::bad_database("Invalid PDU in database."))?; | ||||||
|  | 
 | ||||||
|  |                 if content.membership == MembershipState::Leave { | ||||||
|  |                     left_since_last_sync = true; | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if left_since_last_sync { | ||||||
|  |             device_list_left.extend( | ||||||
|  |                 db.rooms | ||||||
|  |                     .room_members(&room_id) | ||||||
|  |                     .filter_map(|user_id| { | ||||||
|  |                         Some( | ||||||
|  |                             UserId::try_from(user_id.ok()?.clone()) | ||||||
|  |                                 .map_err(|_| { | ||||||
|  |                                     Error::bad_database("Invalid member event state key in db.") | ||||||
|  |                                 }) | ||||||
|  |                                 .ok()?, | ||||||
|  |                         ) | ||||||
|  |                     }) | ||||||
|  |                     .filter(|user_id| { | ||||||
|  |                         // Don't send key updates from the sender to the sender
 | ||||||
|  |                         sender_id != user_id | ||||||
|  |                     }) | ||||||
|  |                     .filter(|user_id| { | ||||||
|  |                         // Only send if the sender doesn't share any encrypted room with the target
 | ||||||
|  |                         // anymore
 | ||||||
|  |                         !share_encrypted_room(&db, sender_id, user_id, &room_id) | ||||||
|  |                     }), | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         if !left_room.is_empty() { |         if !left_room.is_empty() { | ||||||
|             left_rooms.insert(room_id.clone(), left_room); |             left_rooms.insert(room_id.clone(), left_room); | ||||||
|         } |         } | ||||||
|  | @ -392,23 +453,19 @@ pub async fn sync_events_route( | ||||||
|     for room_id in db.rooms.rooms_invited(&sender_id) { |     for room_id in db.rooms.rooms_invited(&sender_id) { | ||||||
|         let room_id = room_id?; |         let room_id = room_id?; | ||||||
|         let mut invited_since_last_sync = false; |         let mut invited_since_last_sync = false; | ||||||
|         for pdu in db |         for pdu in db.rooms.pdus_since(&sender_id, &room_id, since)? { | ||||||
|             .rooms |  | ||||||
|             .pdus_since(&sender_id, &room_id, since)? |  | ||||||
|         { |  | ||||||
|             let pdu = pdu?; |             let pdu = pdu?; | ||||||
|             if pdu.kind == EventType::RoomMember { |             if pdu.kind == EventType::RoomMember && pdu.state_key == Some(sender_id.to_string()) { | ||||||
|                 if pdu.state_key == Some(sender_id.to_string()) { |                 let content = serde_json::from_value::< | ||||||
|                     let content = serde_json::from_value::< |                     Raw<ruma::events::room::member::MemberEventContent>, | ||||||
|                         Raw<ruma::events::room::member::MemberEventContent>, |                 >(pdu.content.clone()) | ||||||
|                     >(pdu.content.clone()) |                 .expect("Raw::from_value always works") | ||||||
|                     .expect("Raw::from_value always works") |                 .deserialize() | ||||||
|                     .deserialize() |                 .map_err(|_| Error::bad_database("Invalid PDU in database."))?; | ||||||
|                     .map_err(|_| Error::bad_database("Invalid PDU in database."))?; | 
 | ||||||
|                     if content.membership == ruma::events::room::member::MembershipState::Invite { |                 if content.membership == MembershipState::Invite { | ||||||
|                         invited_since_last_sync = true; |                     invited_since_last_sync = true; | ||||||
|                         break; |                     break; | ||||||
|                     } |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | @ -433,6 +490,27 @@ pub async fn sync_events_route( | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     for user_id in left_encrypted_users { | ||||||
|  |         // If the user doesn't share an encrypted room with the target anymore, we need to tell
 | ||||||
|  |         // them
 | ||||||
|  |         if db | ||||||
|  |             .rooms | ||||||
|  |             .get_shared_rooms(vec![sender_id.clone(), user_id.clone()]) | ||||||
|  |             .filter_map(|r| r.ok()) | ||||||
|  |             .filter_map(|other_room_id| { | ||||||
|  |                 Some( | ||||||
|  |                     db.rooms | ||||||
|  |                         .room_state_get(&other_room_id, &EventType::RoomEncryption, "") | ||||||
|  |                         .ok()? | ||||||
|  |                         .is_some(), | ||||||
|  |                 ) | ||||||
|  |             }) | ||||||
|  |             .all(|encrypted| !encrypted) | ||||||
|  |         { | ||||||
|  |             device_list_left.insert(user_id); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // Remove all to-device events the device received *last time*
 |     // Remove all to-device events the device received *last time*
 | ||||||
|     db.users |     db.users | ||||||
|         .remove_to_device_events(sender_id, device_id, since)?; |         .remove_to_device_events(sender_id, device_id, since)?; | ||||||
|  | @ -464,7 +542,7 @@ pub async fn sync_events_route( | ||||||
|         }, |         }, | ||||||
|         device_lists: sync_events::DeviceLists { |         device_lists: sync_events::DeviceLists { | ||||||
|             changed: device_list_updates.into_iter().collect(), |             changed: device_list_updates.into_iter().collect(), | ||||||
|             left: Vec::new(), // TODO
 |             left: device_list_left.into_iter().collect(), | ||||||
|         }, |         }, | ||||||
|         device_one_time_keys_count: if db.users.last_one_time_keys_update(sender_id)? > since { |         device_one_time_keys_count: if db.users.last_one_time_keys_update(sender_id)? > since { | ||||||
|             db.users.count_one_time_keys(sender_id, device_id)? |             db.users.count_one_time_keys(sender_id, device_id)? | ||||||
|  | @ -500,3 +578,24 @@ pub async fn sync_events_route( | ||||||
| 
 | 
 | ||||||
|     Ok(response.into()) |     Ok(response.into()) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | fn share_encrypted_room( | ||||||
|  |     db: &Database, | ||||||
|  |     sender_id: &UserId, | ||||||
|  |     user_id: &UserId, | ||||||
|  |     ignore_room: &RoomId, | ||||||
|  | ) -> bool { | ||||||
|  |     db.rooms | ||||||
|  |         .get_shared_rooms(vec![sender_id.clone(), user_id.clone()]) | ||||||
|  |         .filter_map(|r| r.ok()) | ||||||
|  |         .filter(|room_id| room_id != ignore_room) | ||||||
|  |         .filter_map(|other_room_id| { | ||||||
|  |             Some( | ||||||
|  |                 db.rooms | ||||||
|  |                     .room_state_get(&other_room_id, &EventType::RoomEncryption, "") | ||||||
|  |                     .ok()? | ||||||
|  |                     .is_some(), | ||||||
|  |             ) | ||||||
|  |         }) | ||||||
|  |         .any(|encrypted| encrypted) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -5,6 +5,16 @@ use std::collections::BTreeMap; | ||||||
| #[cfg(feature = "conduit_bin")] | #[cfg(feature = "conduit_bin")] | ||||||
| use rocket::get; | use rocket::get; | ||||||
| 
 | 
 | ||||||
|  | /// # `GET /_matrix/client/versions`
 | ||||||
|  | ///
 | ||||||
|  | /// Get the versions of the specification and unstable features supported by this server.
 | ||||||
|  | ///
 | ||||||
|  | /// - Versions take the form MAJOR.MINOR.PATCH
 | ||||||
|  | /// - Only the latest PATCH release will be reported for each MAJOR.MINOR value
 | ||||||
|  | /// - Unstable features should be namespaced and may include version information in their name
 | ||||||
|  | ///
 | ||||||
|  | /// Note: Unstable features are used while developing new features. Clients should avoid using
 | ||||||
|  | /// unstable features in their stable releases
 | ||||||
| #[cfg_attr(feature = "conduit_bin", get("/_matrix/client/versions"))] | #[cfg_attr(feature = "conduit_bin", get("/_matrix/client/versions"))] | ||||||
| pub fn get_supported_versions_route() -> ConduitResult<get_supported_versions::Response> { | pub fn get_supported_versions_route() -> ConduitResult<get_supported_versions::Response> { | ||||||
|     let mut unstable_features = BTreeMap::new(); |     let mut unstable_features = BTreeMap::new(); | ||||||
|  |  | ||||||
|  | @ -104,6 +104,8 @@ impl Database { | ||||||
|                 aliasid_alias: db.open_tree("alias_roomid")?, |                 aliasid_alias: db.open_tree("alias_roomid")?, | ||||||
|                 publicroomids: db.open_tree("publicroomids")?, |                 publicroomids: db.open_tree("publicroomids")?, | ||||||
| 
 | 
 | ||||||
|  |                 tokenids: db.open_tree("tokenids")?, | ||||||
|  | 
 | ||||||
|                 userroomid_joined: db.open_tree("userroomid_joined")?, |                 userroomid_joined: db.open_tree("userroomid_joined")?, | ||||||
|                 roomuserid_joined: db.open_tree("roomuserid_joined")?, |                 roomuserid_joined: db.open_tree("roomuserid_joined")?, | ||||||
|                 userroomid_invited: db.open_tree("userroomid_invited")?, |                 userroomid_invited: db.open_tree("userroomid_invited")?, | ||||||
|  | @ -127,7 +129,6 @@ impl Database { | ||||||
| 
 | 
 | ||||||
|     pub async fn watch(&self, user_id: &UserId, device_id: &DeviceId) { |     pub async fn watch(&self, user_id: &UserId, device_id: &DeviceId) { | ||||||
|         let userid_bytes = user_id.to_string().as_bytes().to_vec(); |         let userid_bytes = user_id.to_string().as_bytes().to_vec(); | ||||||
| 
 |  | ||||||
|         let mut userid_prefix = userid_bytes.clone(); |         let mut userid_prefix = userid_bytes.clone(); | ||||||
|         userid_prefix.push(0xff); |         userid_prefix.push(0xff); | ||||||
| 
 | 
 | ||||||
|  | @ -151,7 +152,8 @@ impl Database { | ||||||
| 
 | 
 | ||||||
|         // Events for rooms we are in
 |         // Events for rooms we are in
 | ||||||
|         for room_id in self.rooms.rooms_joined(user_id).filter_map(|r| r.ok()) { |         for room_id in self.rooms.rooms_joined(user_id).filter_map(|r| r.ok()) { | ||||||
|             let mut roomid_prefix = room_id.to_string().as_bytes().to_vec(); |             let roomid_bytes = room_id.to_string().as_bytes().to_vec(); | ||||||
|  |             let mut roomid_prefix = roomid_bytes.clone(); | ||||||
|             roomid_prefix.push(0xff); |             roomid_prefix.push(0xff); | ||||||
| 
 | 
 | ||||||
|             // PDUs
 |             // PDUs
 | ||||||
|  | @ -162,7 +164,7 @@ impl Database { | ||||||
|                 self.rooms |                 self.rooms | ||||||
|                     .edus |                     .edus | ||||||
|                     .roomid_lastroomactiveupdate |                     .roomid_lastroomactiveupdate | ||||||
|                     .watch_prefix(&roomid_prefix), |                     .watch_prefix(&roomid_bytes), | ||||||
|             ); |             ); | ||||||
| 
 | 
 | ||||||
|             futures.push( |             futures.push( | ||||||
|  |  | ||||||
|  | @ -35,6 +35,8 @@ pub struct Rooms { | ||||||
|     pub(super) aliasid_alias: sled::Tree, // AliasId = RoomId + Count
 |     pub(super) aliasid_alias: sled::Tree, // AliasId = RoomId + Count
 | ||||||
|     pub(super) publicroomids: sled::Tree, |     pub(super) publicroomids: sled::Tree, | ||||||
| 
 | 
 | ||||||
|  |     pub(super) tokenids: sled::Tree, // TokenId = RoomId + Token + PduId
 | ||||||
|  | 
 | ||||||
|     pub(super) userroomid_joined: sled::Tree, |     pub(super) userroomid_joined: sled::Tree, | ||||||
|     pub(super) roomuserid_joined: sled::Tree, |     pub(super) roomuserid_joined: sled::Tree, | ||||||
|     pub(super) userroomid_invited: sled::Tree, |     pub(super) userroomid_invited: sled::Tree, | ||||||
|  | @ -562,7 +564,7 @@ impl Rooms { | ||||||
|         self.pduid_pdu.insert(&pdu_id, &*pdu_json.to_string())?; |         self.pduid_pdu.insert(&pdu_id, &*pdu_json.to_string())?; | ||||||
| 
 | 
 | ||||||
|         self.eventid_pduid |         self.eventid_pduid | ||||||
|             .insert(pdu.event_id.to_string(), pdu_id)?; |             .insert(pdu.event_id.to_string(), pdu_id.clone())?; | ||||||
| 
 | 
 | ||||||
|         if let Some(state_key) = pdu.state_key { |         if let Some(state_key) = pdu.state_key { | ||||||
|             let mut key = room_id.to_string().as_bytes().to_vec(); |             let mut key = room_id.to_string().as_bytes().to_vec(); | ||||||
|  | @ -616,6 +618,21 @@ impl Rooms { | ||||||
|                     )?; |                     )?; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |             EventType::RoomMessage => { | ||||||
|  |                 if let Some(body) = content.get("body").and_then(|b| b.as_str()) { | ||||||
|  |                     for word in body | ||||||
|  |                         .split_terminator(|c: char| !c.is_alphanumeric()) | ||||||
|  |                         .map(str::to_lowercase) | ||||||
|  |                     { | ||||||
|  |                         let mut key = room_id.to_string().as_bytes().to_vec(); | ||||||
|  |                         key.push(0xff); | ||||||
|  |                         key.extend_from_slice(word.as_bytes()); | ||||||
|  |                         key.push(0xff); | ||||||
|  |                         key.extend_from_slice(&pdu_id); | ||||||
|  |                         self.tokenids.insert(key, &[])?; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|             _ => {} |             _ => {} | ||||||
|         } |         } | ||||||
|         self.edus.room_read_set(&room_id, &sender, index)?; |         self.edus.room_read_set(&room_id, &sender, index)?; | ||||||
|  | @ -928,6 +945,95 @@ impl Rooms { | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     pub fn search_pdus<'a>( | ||||||
|  |         &'a self, | ||||||
|  |         room_id: &RoomId, | ||||||
|  |         search_string: &str, | ||||||
|  |     ) -> Result<(impl Iterator<Item = IVec> + 'a, Vec<String>)> { | ||||||
|  |         let mut prefix = room_id.to_string().as_bytes().to_vec(); | ||||||
|  |         prefix.push(0xff); | ||||||
|  | 
 | ||||||
|  |         let words = search_string | ||||||
|  |             .split_terminator(|c: char| !c.is_alphanumeric()) | ||||||
|  |             .map(str::to_lowercase) | ||||||
|  |             .collect::<Vec<_>>(); | ||||||
|  | 
 | ||||||
|  |         let iterators = words.clone().into_iter().map(move |word| { | ||||||
|  |             let mut prefix2 = prefix.clone(); | ||||||
|  |             prefix2.extend_from_slice(word.as_bytes()); | ||||||
|  |             prefix2.push(0xff); | ||||||
|  |             self.tokenids | ||||||
|  |                 .scan_prefix(&prefix2) | ||||||
|  |                 .keys() | ||||||
|  |                 .rev() // Newest pdus first
 | ||||||
|  |                 .filter_map(|r| r.ok()) | ||||||
|  |                 .map(|key| { | ||||||
|  |                     let pduid_index = key | ||||||
|  |                         .iter() | ||||||
|  |                         .enumerate() | ||||||
|  |                         .filter(|(_, &b)| b == 0xff) | ||||||
|  |                         .nth(1) | ||||||
|  |                         .ok_or_else(|| Error::bad_database("Invalid tokenid in db."))? | ||||||
|  |                         .0 | ||||||
|  |                         + 1; // +1 because the pdu id starts AFTER the separator
 | ||||||
|  | 
 | ||||||
|  |                     let pdu_id = key.subslice(pduid_index, key.len() - pduid_index); | ||||||
|  | 
 | ||||||
|  |                     Ok::<_, Error>(pdu_id) | ||||||
|  |                 }) | ||||||
|  |                 .filter_map(|r| r.ok()) | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         Ok(( | ||||||
|  |             utils::common_elements(iterators, |a, b| { | ||||||
|  |                 // We compare b with a because we reversed the iterator earlier
 | ||||||
|  |                 b.cmp(a) | ||||||
|  |             }) | ||||||
|  |             .unwrap(), | ||||||
|  |             words, | ||||||
|  |         )) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn get_shared_rooms<'a>( | ||||||
|  |         &'a self, | ||||||
|  |         users: Vec<UserId>, | ||||||
|  |     ) -> impl Iterator<Item = Result<RoomId>> + 'a { | ||||||
|  |         let iterators = users.into_iter().map(move |user_id| { | ||||||
|  |             let mut prefix = user_id.as_bytes().to_vec(); | ||||||
|  |             prefix.push(0xff); | ||||||
|  | 
 | ||||||
|  |             self.userroomid_joined | ||||||
|  |                 .scan_prefix(&prefix) | ||||||
|  |                 .keys() | ||||||
|  |                 .filter_map(|r| r.ok()) | ||||||
|  |                 .map(|key| { | ||||||
|  |                     let roomid_index = key | ||||||
|  |                         .iter() | ||||||
|  |                         .enumerate() | ||||||
|  |                         .filter(|(_, &b)| b == 0xff) | ||||||
|  |                         .nth(0) | ||||||
|  |                         .ok_or_else(|| Error::bad_database("Invalid userroomid_joined in db."))? | ||||||
|  |                         .0 | ||||||
|  |                         + 1; // +1 because the room id starts AFTER the separator
 | ||||||
|  | 
 | ||||||
|  |                     let room_id = key.subslice(roomid_index, key.len() - roomid_index); | ||||||
|  | 
 | ||||||
|  |                     Ok::<_, Error>(room_id) | ||||||
|  |                 }) | ||||||
|  |                 .filter_map(|r| r.ok()) | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // We use the default compare function because keys are sorted correctly (not reversed)
 | ||||||
|  |         utils::common_elements(iterators, Ord::cmp) | ||||||
|  |             .expect("users is not empty") | ||||||
|  |             .map(|bytes| { | ||||||
|  |                 RoomId::try_from(utils::string_from_bytes(&*bytes).map_err(|_| { | ||||||
|  |                     Error::bad_database("Invalid RoomId bytes in userroomid_joined") | ||||||
|  |                 })?) | ||||||
|  |                 .map_err(|_| Error::bad_database("Invalid RoomId in userroomid_joined.")) | ||||||
|  |             }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /// Returns an iterator over all joined members of a room.
 |     /// Returns an iterator over all joined members of a room.
 | ||||||
|     pub fn room_members(&self, room_id: &RoomId) -> impl Iterator<Item = Result<UserId>> { |     pub fn room_members(&self, room_id: &RoomId) -> impl Iterator<Item = Result<UserId>> { | ||||||
|         self.roomuserid_joined |         self.roomuserid_joined | ||||||
|  |  | ||||||
|  | @ -408,19 +408,7 @@ impl Users { | ||||||
|             &*serde_json::to_string(&device_keys).expect("DeviceKeys::to_string always works"), |             &*serde_json::to_string(&device_keys).expect("DeviceKeys::to_string always works"), | ||||||
|         )?; |         )?; | ||||||
| 
 | 
 | ||||||
|         let count = globals.next_count()?.to_be_bytes(); |         self.mark_device_key_update(user_id, rooms, globals)?; | ||||||
|         for room_id in rooms.rooms_joined(&user_id) { |  | ||||||
|             let mut key = room_id?.to_string().as_bytes().to_vec(); |  | ||||||
|             key.push(0xff); |  | ||||||
|             key.extend_from_slice(&count); |  | ||||||
| 
 |  | ||||||
|             self.keychangeid_userid.insert(key, &*user_id.to_string())?; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let mut key = user_id.to_string().as_bytes().to_vec(); |  | ||||||
|         key.push(0xff); |  | ||||||
|         key.extend_from_slice(&count); |  | ||||||
|         self.keychangeid_userid.insert(key, &*user_id.to_string())?; |  | ||||||
| 
 | 
 | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
|  | @ -520,19 +508,7 @@ impl Users { | ||||||
|                 .insert(&*user_id.to_string(), user_signing_key_key)?; |                 .insert(&*user_id.to_string(), user_signing_key_key)?; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let count = globals.next_count()?.to_be_bytes(); |         self.mark_device_key_update(user_id, rooms, globals)?; | ||||||
|         for room_id in rooms.rooms_joined(&user_id) { |  | ||||||
|             let mut key = room_id?.to_string().as_bytes().to_vec(); |  | ||||||
|             key.push(0xff); |  | ||||||
|             key.extend_from_slice(&count); |  | ||||||
| 
 |  | ||||||
|             self.keychangeid_userid.insert(key, &*user_id.to_string())?; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let mut key = user_id.to_string().as_bytes().to_vec(); |  | ||||||
|         key.push(0xff); |  | ||||||
|         key.extend_from_slice(&count); |  | ||||||
|         self.keychangeid_userid.insert(key, &*user_id.to_string())?; |  | ||||||
| 
 | 
 | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
|  | @ -576,21 +552,7 @@ impl Users { | ||||||
|         )?; |         )?; | ||||||
| 
 | 
 | ||||||
|         // TODO: Should we notify about this change?
 |         // TODO: Should we notify about this change?
 | ||||||
|         let count = globals.next_count()?.to_be_bytes(); |         self.mark_device_key_update(target_id, rooms, globals)?; | ||||||
|         for room_id in rooms.rooms_joined(&target_id) { |  | ||||||
|             let mut key = room_id?.to_string().as_bytes().to_vec(); |  | ||||||
|             key.push(0xff); |  | ||||||
|             key.extend_from_slice(&count); |  | ||||||
| 
 |  | ||||||
|             self.keychangeid_userid |  | ||||||
|                 .insert(key, &*target_id.to_string())?; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let mut key = target_id.to_string().as_bytes().to_vec(); |  | ||||||
|         key.push(0xff); |  | ||||||
|         key.extend_from_slice(&count); |  | ||||||
|         self.keychangeid_userid |  | ||||||
|             .insert(key, &*target_id.to_string())?; |  | ||||||
| 
 | 
 | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
|  | @ -628,6 +590,37 @@ impl Users { | ||||||
|             }) |             }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     fn mark_device_key_update( | ||||||
|  |         &self, | ||||||
|  |         user_id: &UserId, | ||||||
|  |         rooms: &super::rooms::Rooms, | ||||||
|  |         globals: &super::globals::Globals, | ||||||
|  |     ) -> Result<()> { | ||||||
|  |         let count = globals.next_count()?.to_be_bytes(); | ||||||
|  |         for room_id in rooms.rooms_joined(&user_id).filter_map(|r| r.ok()) { | ||||||
|  |             // Don't send key updates to unencrypted rooms
 | ||||||
|  |             if rooms | ||||||
|  |                 .room_state_get(&room_id, &EventType::RoomEncryption, "")? | ||||||
|  |                 .is_none() | ||||||
|  |             { | ||||||
|  |                 return Ok(()); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             let mut key = room_id.to_string().as_bytes().to_vec(); | ||||||
|  |             key.push(0xff); | ||||||
|  |             key.extend_from_slice(&count); | ||||||
|  | 
 | ||||||
|  |             self.keychangeid_userid.insert(key, &*user_id.to_string())?; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let mut key = user_id.to_string().as_bytes().to_vec(); | ||||||
|  |         key.push(0xff); | ||||||
|  |         key.extend_from_slice(&count); | ||||||
|  |         self.keychangeid_userid.insert(key, &*user_id.to_string())?; | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     pub fn get_device_keys( |     pub fn get_device_keys( | ||||||
|         &self, |         &self, | ||||||
|         user_id: &UserId, |         user_id: &UserId, | ||||||
|  | @ -859,7 +852,9 @@ impl Users { | ||||||
|             self.remove_device(&user_id, &device_id?)?; |             self.remove_device(&user_id, &device_id?)?; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Set the password to "" to indicate a deactivated account
 |         // Set the password to "" to indicate a deactivated account. Hashes will never result in an
 | ||||||
|  |         // empty string, so the user will not be able to log in again. Systems like changing the
 | ||||||
|  |         // password without logging in should check if the account is deactivated.
 | ||||||
|         self.userid_password.insert(user_id.to_string(), "")?; |         self.userid_password.insert(user_id.to_string(), "")?; | ||||||
| 
 | 
 | ||||||
|         // TODO: Unhook 3PID
 |         // TODO: Unhook 3PID
 | ||||||
|  |  | ||||||
|  | @ -27,6 +27,13 @@ pub enum Error { | ||||||
|         #[from] |         #[from] | ||||||
|         source: image::error::ImageError, |         source: image::error::ImageError, | ||||||
|     }, |     }, | ||||||
|  |     #[error("Could not connect to server.")] | ||||||
|  |     ReqwestError { | ||||||
|  |         #[from] | ||||||
|  |         source: reqwest::Error, | ||||||
|  |     }, | ||||||
|  |     #[error("{0}")] | ||||||
|  |     BadServerResponse(&'static str), | ||||||
|     #[error("{0}")] |     #[error("{0}")] | ||||||
|     BadConfig(&'static str), |     BadConfig(&'static str), | ||||||
|     #[error("{0}")] |     #[error("{0}")] | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ mod error; | ||||||
| mod pdu; | mod pdu; | ||||||
| mod push_rules; | mod push_rules; | ||||||
| mod ruma_wrapper; | mod ruma_wrapper; | ||||||
|  | pub mod server_server; | ||||||
| mod utils; | mod utils; | ||||||
| 
 | 
 | ||||||
| pub use database::Database; | pub use database::Database; | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/main.rs
									
									
									
									
									
								
							|  | @ -1,13 +1,13 @@ | ||||||
| #![warn(rust_2018_idioms)] | #![warn(rust_2018_idioms)] | ||||||
| 
 | 
 | ||||||
| pub mod push_rules; | pub mod client_server; | ||||||
|  | pub mod server_server; | ||||||
| 
 | 
 | ||||||
| mod client_server; |  | ||||||
| mod database; | mod database; | ||||||
| mod error; | mod error; | ||||||
| mod pdu; | mod pdu; | ||||||
|  | mod push_rules; | ||||||
| mod ruma_wrapper; | mod ruma_wrapper; | ||||||
| //mod server_server;
 |  | ||||||
| mod utils; | mod utils; | ||||||
| 
 | 
 | ||||||
| pub use database::Database; | pub use database::Database; | ||||||
|  | @ -26,7 +26,7 @@ fn setup_rocket() -> rocket::Rocket { | ||||||
|                 client_server::get_supported_versions_route, |                 client_server::get_supported_versions_route, | ||||||
|                 client_server::get_register_available_route, |                 client_server::get_register_available_route, | ||||||
|                 client_server::register_route, |                 client_server::register_route, | ||||||
|                 client_server::get_login_route, |                 client_server::get_login_types_route, | ||||||
|                 client_server::login_route, |                 client_server::login_route, | ||||||
|                 client_server::whoami_route, |                 client_server::whoami_route, | ||||||
|                 client_server::logout_route, |                 client_server::logout_route, | ||||||
|  | @ -90,6 +90,7 @@ fn setup_rocket() -> rocket::Rocket { | ||||||
|                 client_server::sync_events_route, |                 client_server::sync_events_route, | ||||||
|                 client_server::get_context_route, |                 client_server::get_context_route, | ||||||
|                 client_server::get_message_events_route, |                 client_server::get_message_events_route, | ||||||
|  |                 client_server::search_events_route, | ||||||
|                 client_server::turn_server_route, |                 client_server::turn_server_route, | ||||||
|                 client_server::send_event_to_device_route, |                 client_server::send_event_to_device_route, | ||||||
|                 client_server::get_media_config_route, |                 client_server::get_media_config_route, | ||||||
|  | @ -110,10 +111,12 @@ fn setup_rocket() -> rocket::Rocket { | ||||||
|                 client_server::get_key_changes_route, |                 client_server::get_key_changes_route, | ||||||
|                 client_server::get_pushers_route, |                 client_server::get_pushers_route, | ||||||
|                 client_server::set_pushers_route, |                 client_server::set_pushers_route, | ||||||
|                 //server_server::well_known_server,
 |                 server_server::well_known_server, | ||||||
|                 //server_server::get_server_version,
 |                 server_server::get_server_version, | ||||||
|                 //server_server::get_server_keys,
 |                 server_server::get_server_keys, | ||||||
|                 //server_server::get_server_keys_deprecated,
 |                 server_server::get_server_keys_deprecated, | ||||||
|  |                 server_server::get_public_rooms_route, | ||||||
|  |                 server_server::send_transaction_message_route, | ||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
|         .attach(AdHoc::on_attach("Config", |mut rocket| async { |         .attach(AdHoc::on_attach("Config", |mut rocket| async { | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								src/pdu.rs
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/pdu.rs
									
									
									
									
									
								
							|  | @ -37,11 +37,12 @@ pub struct PduEvent { | ||||||
| impl PduEvent { | impl PduEvent { | ||||||
|     pub fn redact(&mut self) -> Result<()> { |     pub fn redact(&mut self) -> Result<()> { | ||||||
|         self.unsigned.clear(); |         self.unsigned.clear(); | ||||||
|         let allowed = match self.kind { | 
 | ||||||
|             EventType::RoomMember => vec!["membership"], |         let allowed: &[&str] = match self.kind { | ||||||
|             EventType::RoomCreate => vec!["creator"], |             EventType::RoomMember => &["membership"], | ||||||
|             EventType::RoomJoinRules => vec!["join_rule"], |             EventType::RoomCreate => &["creator"], | ||||||
|             EventType::RoomPowerLevels => vec![ |             EventType::RoomJoinRules => &["join_rule"], | ||||||
|  |             EventType::RoomPowerLevels => &[ | ||||||
|                 "ban", |                 "ban", | ||||||
|                 "events", |                 "events", | ||||||
|                 "events_default", |                 "events_default", | ||||||
|  | @ -51,8 +52,8 @@ impl PduEvent { | ||||||
|                 "users", |                 "users", | ||||||
|                 "users_default", |                 "users_default", | ||||||
|             ], |             ], | ||||||
|             EventType::RoomHistoryVisibility => vec!["history_visibility"], |             EventType::RoomHistoryVisibility => &["history_visibility"], | ||||||
|             _ => vec![], |             _ => &[], | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let old_content = self |         let old_content = self | ||||||
|  | @ -63,8 +64,8 @@ impl PduEvent { | ||||||
|         let mut new_content = serde_json::Map::new(); |         let mut new_content = serde_json::Map::new(); | ||||||
| 
 | 
 | ||||||
|         for key in allowed { |         for key in allowed { | ||||||
|             if let Some(value) = old_content.remove(key) { |             if let Some(value) = old_content.remove(*key) { | ||||||
|                 new_content.insert(key.to_owned(), value); |                 new_content.insert((*key).to_owned(), value); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,16 +1,19 @@ | ||||||
| use crate::{Database, MatrixResult}; | use crate::{client_server, ConduitResult, Database, Error, Result, Ruma}; | ||||||
| use http::header::{HeaderValue, AUTHORIZATION}; | use http::header::{HeaderValue, AUTHORIZATION}; | ||||||
| use log::error; | use rocket::{get, post, put, response::content::Json, State}; | ||||||
| use rocket::{get, response::content::Json, State}; | use ruma::api::federation::{ | ||||||
| use ruma::api::Endpoint; |     directory::get_public_rooms, | ||||||
| use ruma::api::client::error::Error; |     discovery::{ | ||||||
| use ruma::api::federation::discovery::{ |         get_server_keys, get_server_version::v1 as get_server_version, ServerKey, VerifyKey, | ||||||
|     get_server_keys::v2 as get_server_keys, get_server_version::v1 as get_server_version, |     }, | ||||||
|  |     transactions::send_transaction_message, | ||||||
| }; | }; | ||||||
|  | use ruma::api::{client, OutgoingRequest}; | ||||||
| use serde_json::json; | use serde_json::json; | ||||||
| use std::{ | use std::{ | ||||||
|     collections::BTreeMap, |     collections::BTreeMap, | ||||||
|     convert::TryFrom, |     convert::TryFrom, | ||||||
|  |     fmt::Debug, | ||||||
|     time::{Duration, SystemTime}, |     time::{Duration, SystemTime}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -33,36 +36,51 @@ pub async fn request_well_known(db: &crate::Database, destination: &str) -> Opti | ||||||
|     Some(body.get("m.server")?.as_str()?.to_owned()) |     Some(body.get("m.server")?.as_str()?.to_owned()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub async fn send_request<T: Endpoint>( | pub async fn send_request<T: OutgoingRequest>( | ||||||
|     db: &crate::Database, |     db: &crate::Database, | ||||||
|     destination: String, |     destination: String, | ||||||
|     request: T, |     request: T, | ||||||
| ) -> Option<T::Response> { | ) -> Result<T::IncomingResponse> | ||||||
|     let mut http_request: http::Request<_> = request.try_into().unwrap(); | where | ||||||
| 
 |     T: Debug, | ||||||
|  | { | ||||||
|     let actual_destination = "https://".to_owned() |     let actual_destination = "https://".to_owned() | ||||||
|         + &request_well_known(db, &destination) |         + &request_well_known(db, &destination) | ||||||
|             .await |             .await | ||||||
|             .unwrap_or(destination.clone() + ":8448"); |             .unwrap_or(destination.clone() + ":8448"); | ||||||
|     *http_request.uri_mut() = (actual_destination + T::METADATA.path).parse().unwrap(); | 
 | ||||||
|  |     let mut http_request = request | ||||||
|  |         .try_into_http_request(&actual_destination, Some("")) | ||||||
|  |         .unwrap(); | ||||||
| 
 | 
 | ||||||
|     let mut request_map = serde_json::Map::new(); |     let mut request_map = serde_json::Map::new(); | ||||||
| 
 | 
 | ||||||
|     if !http_request.body().is_empty() { |     if !http_request.body().is_empty() { | ||||||
|         request_map.insert( |         request_map.insert( | ||||||
|             "content".to_owned(), |             "content".to_owned(), | ||||||
|             serde_json::to_value(http_request.body()).unwrap(), |             serde_json::from_slice(http_request.body()).unwrap(), | ||||||
|         ); |         ); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     request_map.insert("method".to_owned(), T::METADATA.method.to_string().into()); |     request_map.insert("method".to_owned(), T::METADATA.method.to_string().into()); | ||||||
|     request_map.insert("uri".to_owned(), T::METADATA.path.into()); |     request_map.insert( | ||||||
|     request_map.insert("origin".to_owned(), db.globals.server_name().into()); |         "uri".to_owned(), | ||||||
|  |         http_request | ||||||
|  |             .uri() | ||||||
|  |             .path_and_query() | ||||||
|  |             .expect("all requests have a path") | ||||||
|  |             .to_string() | ||||||
|  |             .into(), | ||||||
|  |     ); | ||||||
|  |     request_map.insert( | ||||||
|  |         "origin".to_owned(), | ||||||
|  |         db.globals.server_name().as_str().into(), | ||||||
|  |     ); | ||||||
|     request_map.insert("destination".to_owned(), destination.into()); |     request_map.insert("destination".to_owned(), destination.into()); | ||||||
| 
 | 
 | ||||||
|     let mut request_json = request_map.into(); |     let mut request_json = request_map.into(); | ||||||
|     ruma::signatures::sign_json( |     ruma::signatures::sign_json( | ||||||
|         db.globals.server_name(), |         db.globals.server_name().as_str(), | ||||||
|         db.globals.keypair(), |         db.globals.keypair(), | ||||||
|         &mut request_json, |         &mut request_json, | ||||||
|     ) |     ) | ||||||
|  | @ -72,31 +90,32 @@ pub async fn send_request<T: Endpoint>( | ||||||
|         .as_object() |         .as_object() | ||||||
|         .unwrap() |         .unwrap() | ||||||
|         .values() |         .values() | ||||||
|         .next() |         .map(|v| { | ||||||
|         .unwrap() |             v.as_object() | ||||||
|         .as_object() |                 .unwrap() | ||||||
|         .unwrap() |                 .iter() | ||||||
|         .iter() |                 .map(|(k, v)| (k, v.as_str().unwrap())) | ||||||
|         .map(|(k, v)| (k, v.as_str().unwrap())); |         }); | ||||||
| 
 | 
 | ||||||
|     for s in signatures { |     for signature_server in signatures { | ||||||
|         http_request.headers_mut().insert( |         for s in signature_server { | ||||||
|             AUTHORIZATION, |             http_request.headers_mut().insert( | ||||||
|             HeaderValue::from_str(&format!( |                 AUTHORIZATION, | ||||||
|                 "X-Matrix origin={},key=\"{}\",sig=\"{}\"", |                 HeaderValue::from_str(&format!( | ||||||
|                 db.globals.server_name(), |                     "X-Matrix origin={},key=\"{}\",sig=\"{}\"", | ||||||
|                 s.0, |                     db.globals.server_name(), | ||||||
|                 s.1 |                     s.0, | ||||||
|             )) |                     s.1 | ||||||
|             .unwrap(), |                 )) | ||||||
|         ); |                 .unwrap(), | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let reqwest_response = db |     let reqwest_request = reqwest::Request::try_from(http_request) | ||||||
|         .globals |         .expect("all http requests are valid reqwest requests"); | ||||||
|         .reqwest_client() | 
 | ||||||
|         .execute(http_request.into()) |     let reqwest_response = db.globals.reqwest_client().execute(reqwest_request).await; | ||||||
|         .await; |  | ||||||
| 
 | 
 | ||||||
|     // Because reqwest::Response -> http::Response is complicated:
 |     // Because reqwest::Response -> http::Response is complicated:
 | ||||||
|     match reqwest_response { |     match reqwest_response { | ||||||
|  | @ -117,59 +136,56 @@ pub async fn send_request<T: Endpoint>( | ||||||
|                 .unwrap() |                 .unwrap() | ||||||
|                 .into_iter() |                 .into_iter() | ||||||
|                 .collect(); |                 .collect(); | ||||||
|             Some( |             Ok( | ||||||
|                 <T::Response>::try_from(http_response.body(body).unwrap()) |                 T::IncomingResponse::try_from(http_response.body(body).unwrap()) | ||||||
|                     .ok() |                     .expect("TODO: error handle other server errors"), | ||||||
|                     .unwrap(), |  | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|         Err(e) => { |         Err(e) => Err(e.into()), | ||||||
|             error!("{}", e); |  | ||||||
|             None |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[cfg_attr(feature = "conduit_bin",get("/.well-known/matrix/server"))] | #[cfg_attr(feature = "conduit_bin", get("/.well-known/matrix/server"))] | ||||||
| pub fn well_known_server() -> Json<String> { | pub fn well_known_server() -> Json<String> { | ||||||
|     rocket::response::content::Json( |     rocket::response::content::Json(json!({ "m.server": "pc.koesters.xyz:59003"}).to_string()) | ||||||
|         json!({ "m.server": "matrixtesting.koesters.xyz:14004"}).to_string(), |  | ||||||
|     ) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[cfg_attr(feature = "conduit_bin",get("/_matrix/federation/v1/version"))] | #[cfg_attr(feature = "conduit_bin", get("/_matrix/federation/v1/version"))] | ||||||
| pub fn get_server_version() -> MatrixResult<get_server_version::Response, Error> { | pub fn get_server_version() -> ConduitResult<get_server_version::Response> { | ||||||
|     MatrixResult(Ok(get_server_version::Response { |     Ok(get_server_version::Response { | ||||||
|         server: Some(get_server_version::Server { |         server: Some(get_server_version::Server { | ||||||
|             name: Some("Conduit".to_owned()), |             name: Some("Conduit".to_owned()), | ||||||
|             version: Some(env!("CARGO_PKG_VERSION").to_owned()), |             version: Some(env!("CARGO_PKG_VERSION").to_owned()), | ||||||
|         }), |         }), | ||||||
|     })) |     } | ||||||
|  |     .into()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[cfg_attr(feature = "conduit_bin",get("/_matrix/key/v2/server"))] | #[cfg_attr(feature = "conduit_bin", get("/_matrix/key/v2/server"))] | ||||||
| pub fn get_server_keys(db: State<'_, Database>) -> Json<String> { | pub fn get_server_keys(db: State<'_, Database>) -> Json<String> { | ||||||
|     let mut verify_keys = BTreeMap::new(); |     let mut verify_keys = BTreeMap::new(); | ||||||
|     verify_keys.insert( |     verify_keys.insert( | ||||||
|         format!("ed25519:{}", db.globals.keypair().version()), |         format!("ed25519:{}", db.globals.keypair().version()), | ||||||
|         get_server_keys::VerifyKey { |         VerifyKey { | ||||||
|             key: base64::encode_config(db.globals.keypair().public_key(), base64::STANDARD_NO_PAD), |             key: base64::encode_config(db.globals.keypair().public_key(), base64::STANDARD_NO_PAD), | ||||||
|         }, |         }, | ||||||
|     ); |     ); | ||||||
|     let mut response = serde_json::from_slice( |     let mut response = serde_json::from_slice( | ||||||
|         http::Response::try_from(get_server_keys::Response { |         http::Response::try_from(get_server_keys::v2::Response { | ||||||
|             server_name: db.globals.server_name().to_owned(), |             server_key: ServerKey { | ||||||
|             verify_keys, |                 server_name: db.globals.server_name().to_owned(), | ||||||
|             old_verify_keys: BTreeMap::new(), |                 verify_keys, | ||||||
|             signatures: BTreeMap::new(), |                 old_verify_keys: BTreeMap::new(), | ||||||
|             valid_until_ts: SystemTime::now() + Duration::from_secs(60 * 2), |                 signatures: BTreeMap::new(), | ||||||
|  |                 valid_until_ts: SystemTime::now() + Duration::from_secs(60 * 2), | ||||||
|  |             }, | ||||||
|         }) |         }) | ||||||
|         .unwrap() |         .unwrap() | ||||||
|         .body(), |         .body(), | ||||||
|     ) |     ) | ||||||
|     .unwrap(); |     .unwrap(); | ||||||
|     ruma::signatures::sign_json( |     ruma::signatures::sign_json( | ||||||
|         db.globals.server_name(), |         db.globals.server_name().as_str(), | ||||||
|         db.globals.keypair(), |         db.globals.keypair(), | ||||||
|         &mut response, |         &mut response, | ||||||
|     ) |     ) | ||||||
|  | @ -177,7 +193,88 @@ pub fn get_server_keys(db: State<'_, Database>) -> Json<String> { | ||||||
|     Json(response.to_string()) |     Json(response.to_string()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[cfg_attr(feature = "conduit_bin",get("/_matrix/key/v2/server/<_key_id>"))] | #[cfg_attr(feature = "conduit_bin", get("/_matrix/key/v2/server/<_>"))] | ||||||
| pub fn get_server_keys_deprecated(db: State<'_, Database>, _key_id: String) -> Json<String> { | pub fn get_server_keys_deprecated(db: State<'_, Database>) -> Json<String> { | ||||||
|     get_server_keys(db) |     get_server_keys(db) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[cfg_attr(
 | ||||||
|  |     feature = "conduit_bin", | ||||||
|  |     post("/_matrix/federation/v1/publicRooms", data = "<body>") | ||||||
|  | )] | ||||||
|  | pub async fn get_public_rooms_route( | ||||||
|  |     db: State<'_, Database>, | ||||||
|  |     body: Ruma<get_public_rooms::v1::Request>, | ||||||
|  | ) -> ConduitResult<get_public_rooms::v1::Response> { | ||||||
|  |     let Ruma { | ||||||
|  |         body: | ||||||
|  |             get_public_rooms::v1::Request { | ||||||
|  |                 room_network: _room_network, // TODO
 | ||||||
|  |                 limit, | ||||||
|  |                 since, | ||||||
|  |             }, | ||||||
|  |         sender_id, | ||||||
|  |         device_id, | ||||||
|  |         json_body, | ||||||
|  |     } = body; | ||||||
|  | 
 | ||||||
|  |     let client::r0::directory::get_public_rooms_filtered::Response { | ||||||
|  |         chunk, | ||||||
|  |         prev_batch, | ||||||
|  |         next_batch, | ||||||
|  |         total_room_count_estimate, | ||||||
|  |     } = client_server::get_public_rooms_filtered_route( | ||||||
|  |         db, | ||||||
|  |         Ruma { | ||||||
|  |             body: client::r0::directory::get_public_rooms_filtered::IncomingRequest { | ||||||
|  |                 filter: None, | ||||||
|  |                 limit, | ||||||
|  |                 room_network: client::r0::directory::get_public_rooms_filtered::RoomNetwork::Matrix, | ||||||
|  |                 server: None, | ||||||
|  |                 since, | ||||||
|  |             }, | ||||||
|  |             sender_id, | ||||||
|  |             device_id, | ||||||
|  |             json_body, | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     .await? | ||||||
|  |     .0; | ||||||
|  | 
 | ||||||
|  |     Ok(get_public_rooms::v1::Response { | ||||||
|  |         chunk: chunk | ||||||
|  |             .into_iter() | ||||||
|  |             .map(|c| { | ||||||
|  |                 // Convert ruma::api::federation::directory::get_public_rooms::v1::PublicRoomsChunk
 | ||||||
|  |                 // to ruma::api::client::r0::directory::PublicRoomsChunk
 | ||||||
|  |                 Ok::<_, Error>( | ||||||
|  |                     serde_json::from_str( | ||||||
|  |                         &serde_json::to_string(&c) | ||||||
|  |                             .expect("PublicRoomsChunk::to_string always works"), | ||||||
|  |                     ) | ||||||
|  |                     .expect("federation and client-server PublicRoomsChunk are the same type"), | ||||||
|  |                 ) | ||||||
|  |             }) | ||||||
|  |             .filter_map(|r| r.ok()) | ||||||
|  |             .collect(), | ||||||
|  |         prev_batch, | ||||||
|  |         next_batch, | ||||||
|  |         total_room_count_estimate, | ||||||
|  |     } | ||||||
|  |     .into()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg_attr(
 | ||||||
|  |     feature = "conduit_bin", | ||||||
|  |     put("/_matrix/federation/v1/send/<_>", data = "<body>") | ||||||
|  | )] | ||||||
|  | pub fn send_transaction_message_route( | ||||||
|  |     db: State<'_, Database>, | ||||||
|  |     body: Ruma<send_transaction_message::v1::Request>, | ||||||
|  | ) -> ConduitResult<send_transaction_message::v1::Response> { | ||||||
|  |     dbg!(&*body); | ||||||
|  |     Ok(send_transaction_message::v1::Response { | ||||||
|  |         pdus: BTreeMap::new(), | ||||||
|  |     } | ||||||
|  |     .into()) | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										31
									
								
								src/utils.rs
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								src/utils.rs
									
									
									
									
									
								
							|  | @ -1,6 +1,9 @@ | ||||||
| use argon2::{Config, Variant}; | use argon2::{Config, Variant}; | ||||||
|  | use cmp::Ordering; | ||||||
| use rand::prelude::*; | use rand::prelude::*; | ||||||
|  | use sled::IVec; | ||||||
| use std::{ | use std::{ | ||||||
|  |     cmp, | ||||||
|     convert::TryInto, |     convert::TryInto, | ||||||
|     time::{SystemTime, UNIX_EPOCH}, |     time::{SystemTime, UNIX_EPOCH}, | ||||||
| }; | }; | ||||||
|  | @ -59,3 +62,31 @@ pub fn calculate_hash(password: &str) -> Result<String, argon2::Error> { | ||||||
|     let salt = random_string(32); |     let salt = random_string(32); | ||||||
|     argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &hashing_config) |     argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &hashing_config) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | pub fn common_elements( | ||||||
|  |     mut iterators: impl Iterator<Item = impl Iterator<Item = IVec>>, | ||||||
|  |     check_order: impl Fn(&IVec, &IVec) -> Ordering, | ||||||
|  | ) -> Option<impl Iterator<Item = IVec>> { | ||||||
|  |     let first_iterator = iterators.next()?; | ||||||
|  |     let mut other_iterators = iterators.map(|i| i.peekable()).collect::<Vec<_>>(); | ||||||
|  | 
 | ||||||
|  |     Some(first_iterator.filter(move |target| { | ||||||
|  |         other_iterators | ||||||
|  |             .iter_mut() | ||||||
|  |             .map(|it| { | ||||||
|  |                 while let Some(element) = it.peek() { | ||||||
|  |                     match check_order(element, target) { | ||||||
|  |                         Ordering::Greater => return false, // We went too far
 | ||||||
|  |                         Ordering::Equal => return true,    // Element is in both iters
 | ||||||
|  |                         Ordering::Less => { | ||||||
|  |                             // Keep searching
 | ||||||
|  |                             it.next(); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 false | ||||||
|  |             }) | ||||||
|  |             .all(|b| b) | ||||||
|  |     })) | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue