diff --git a/Cargo.lock b/Cargo.lock
index 014c5cc6..0a7334ca 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -267,6 +267,7 @@ checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd"
name = "conduit"
version = "0.1.0"
dependencies = [
+ "base64 0.12.3",
"directories",
"http",
"image",
@@ -1559,7 +1560,7 @@ dependencies = [
[[package]]
name = "ruma"
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 = [
"ruma-api",
"ruma-client-api",
@@ -1573,7 +1574,7 @@ dependencies = [
[[package]]
name = "ruma-api"
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 = [
"http",
"percent-encoding",
@@ -1588,7 +1589,7 @@ dependencies = [
[[package]]
name = "ruma-api-macros"
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 = [
"proc-macro-crate",
"proc-macro2",
@@ -1599,7 +1600,7 @@ dependencies = [
[[package]]
name = "ruma-client-api"
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 = [
"assign",
"http",
@@ -1617,7 +1618,7 @@ dependencies = [
[[package]]
name = "ruma-common"
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 = [
"js_int",
"ruma-identifiers",
@@ -1630,7 +1631,7 @@ dependencies = [
[[package]]
name = "ruma-events"
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 = [
"js_int",
"ruma-common",
@@ -1645,7 +1646,7 @@ dependencies = [
[[package]]
name = "ruma-events-macros"
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 = [
"proc-macro-crate",
"proc-macro2",
@@ -1656,7 +1657,7 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
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 = [
"js_int",
"ruma-api",
@@ -1671,7 +1672,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers"
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 = [
"rand",
"ruma-identifiers-macros",
@@ -1683,7 +1684,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-macros"
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 = [
"proc-macro2",
"quote",
@@ -1694,7 +1695,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
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 = [
"ruma-serde",
"serde",
@@ -1705,7 +1706,7 @@ dependencies = [
[[package]]
name = "ruma-serde"
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 = [
"form_urlencoded",
"itoa",
@@ -1717,7 +1718,7 @@ dependencies = [
[[package]]
name = "ruma-signatures"
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 = [
"base64 0.12.3",
"ring",
diff --git a/Cargo.toml b/Cargo.toml
index 90633ded..4945e3c8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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/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"] }
+tokio = "0.2.22" # Used for long polling
sled = "0.32.0" # Used for storing data permanently
log = "0.4.8" # Used for emitting log entries
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
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
+base64 = "0.12.3" # Used to encode server public key
[features]
default = ["conduit_bin"]
diff --git a/src/client_server/account.rs b/src/client_server/account.rs
index 87644469..9837d1b9 100644
--- a/src/client_server/account.rs
+++ b/src/client_server/account.rs
@@ -15,11 +15,18 @@ use ruma::{
UserId,
};
+use register::RegistrationKind;
#[cfg(feature = "conduit_bin")]
use rocket::{get, post};
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(
feature = "conduit_bin",
get("/_matrix/client/r0/register/available", data = "
")
@@ -53,6 +60,15 @@ pub fn get_register_available_route(
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(
feature = "conduit_bin",
post("/_matrix/client/r0/register", data = "")
@@ -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
let user_id = UserId::parse_with_server_name(
- body.username
- .clone()
- .unwrap_or_else(|| utils::random_string(GUEST_NAME_LENGTH))
- .to_lowercase(),
+ if is_guest {
+ utils::random_string(GUEST_NAME_LENGTH)
+ } else {
+ 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(),
)
.ok()
@@ -84,7 +112,7 @@ pub fn register_route(
))?;
// Check if username is creative enough
- if db.users.exists(&user_id)? {
+ if !missing_username && db.users.exists(&user_id)? {
return Err(Error::BadRequest(
ErrorKind::UserInUse,
"Desired user ID is already taken.",
@@ -116,7 +144,19 @@ pub fn register_route(
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
db.users.create(&user_id, &password)?;
@@ -134,7 +174,7 @@ pub fn register_route(
&db.globals,
)?;
- if body.inhibit_login {
+ if !is_guest && body.inhibit_login {
return Ok(register::Response {
access_token: None,
user_id,
@@ -144,10 +184,12 @@ pub fn register_route(
}
// Generate new device id if the user didn't specify one
- let device_id = body
- .device_id
- .clone()
- .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
+ let device_id = if is_guest {
+ None
+ } else {
+ body.device_id.clone()
+ }
+ .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
// Generate new token for the device
let token = utils::random_string(TOKEN_LENGTH);
@@ -168,6 +210,13 @@ pub fn register_route(
.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(
feature = "conduit_bin",
post("/_matrix/client/r0/account/password", data = "")
@@ -225,6 +274,11 @@ pub fn change_password_route(
Ok(change_password::Response.into())
}
+/// # `GET _matrix/client/r0/account/whoami`
+///
+/// Get user_id of this account.
+///
+/// - Also works for Application Services
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/account/whoami", data = "")
@@ -237,6 +291,14 @@ pub fn whoami_route(body: Ruma) -> ConduitResult", data = "")
)]
-pub fn get_alias_route(
+pub async fn get_alias_route(
db: State<'_, Database>,
body: Ruma,
) -> ConduitResult {
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
diff --git a/src/client_server/capabilities.rs b/src/client_server/capabilities.rs
index afa06048..ddf90f83 100644
--- a/src/client_server/capabilities.rs
+++ b/src/client_server/capabilities.rs
@@ -5,6 +5,9 @@ use std::collections::BTreeMap;
#[cfg(feature = "conduit_bin")]
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"))]
pub fn get_capabilities_route() -> ConduitResult {
let mut available = BTreeMap::new();
diff --git a/src/client_server/directory.rs b/src/client_server/directory.rs
index 279df181..26188f73 100644
--- a/src/client_server/directory.rs
+++ b/src/client_server/directory.rs
@@ -1,15 +1,18 @@
use super::State;
-use crate::{ConduitResult, Database, Error, Result, Ruma};
+use crate::{server_server, ConduitResult, Database, Error, Result, Ruma};
use ruma::{
- api::client::{
- error::ErrorKind,
- r0::{
- directory::{
- self, get_public_rooms, get_public_rooms_filtered, get_room_visibility,
- set_room_visibility,
+ api::{
+ client::{
+ error::ErrorKind,
+ r0::{
+ directory::{
+ self, get_public_rooms, get_public_rooms_filtered, get_room_visibility,
+ set_room_visibility,
+ },
+ room,
},
- room,
},
+ federation,
},
events::{
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>,
body: Ruma,
) -> ConduitResult {
+ 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 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.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::>(),
- );
- */
-
let total_room_count_estimate = (all_rooms.len() as u32).into();
let chunk = all_rooms
diff --git a/src/client_server/membership.rs b/src/client_server/membership.rs
index 0ada7c40..84c0ebd3 100644
--- a/src/client_server/membership.rs
+++ b/src/client_server/membership.rs
@@ -1,16 +1,24 @@
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::{
- api::client::{
- error::ErrorKind,
- r0::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,
+ api::{
+ client::{
+ error::ErrorKind,
+ r0::{
+ alias,
+ 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},
- Raw, RoomId,
+ EventId, Raw, RoomId, RoomVersionId,
};
use std::{collections::BTreeMap, convert::TryFrom};
@@ -21,13 +29,81 @@ use rocket::{get, post};
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/join", data = "")
)]
-pub fn join_room_by_id_route(
+pub async fn join_room_by_id_route(
db: State<'_, Database>,
body: Ruma,
) -> ConduitResult {
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::(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::>(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 {
membership: member::MembershipState::Join,
@@ -61,16 +137,28 @@ pub fn join_room_by_id_route(
feature = "conduit_bin",
post("/_matrix/client/r0/join/<_>", data = "")
)]
-pub fn join_room_by_id_or_alias_route(
+pub async fn join_room_by_id_or_alias_route(
db: State<'_, Database>,
+ db2: State<'_, Database>,
body: Ruma,
) -> ConduitResult {
- let room_id = RoomId::try_from(body.room_id_or_alias.clone()).or_else(|alias| {
- Ok::<_, Error>(db.rooms.id_from_alias(&alias)?.ok_or(Error::BadRequest(
- ErrorKind::NotFound,
- "Room not found (TODO: Federation).",
- ))?)
- })?;
+ let room_id = match RoomId::try_from(body.room_id_or_alias.clone()) {
+ Ok(room_id) => room_id,
+ Err(room_alias) => {
+ 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 {
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 {
- 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())
}
diff --git a/src/client_server/mod.rs b/src/client_server/mod.rs
index 7703198b..e5a36f3a 100644
--- a/src/client_server/mod.rs
+++ b/src/client_server/mod.rs
@@ -17,6 +17,7 @@ mod push;
mod read_marker;
mod redact;
mod room;
+mod search;
mod session;
mod state;
mod sync;
@@ -47,6 +48,7 @@ pub use push::*;
pub use read_marker::*;
pub use redact::*;
pub use room::*;
+pub use search::*;
pub use session::*;
pub use state::*;
pub use sync::*;
diff --git a/src/client_server/room.rs b/src/client_server/room.rs
index 54e57fd8..b5f1529e 100644
--- a/src/client_server/room.rs
+++ b/src/client_server/room.rs
@@ -92,13 +92,6 @@ pub fn create_room_route(
&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
let mut users = BTreeMap::new();
users.insert(sender_id.clone(), 100.into());
@@ -142,6 +135,14 @@ pub fn create_room_route(
)?;
// 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
db.rooms.append_pdu(
PduBuilder {
diff --git a/src/client_server/search.rs b/src/client_server/search.rs
new file mode 100644
index 00000000..dec1ec9f
--- /dev/null
+++ b/src/client_server/search.rs
@@ -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 = "")
+)]
+pub fn search_events_route(
+ db: State<'_, Database>,
+ body: Ruma,
+) -> ConduitResult {
+ 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::>();
+
+ 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())
+}
diff --git a/src/client_server/session.rs b/src/client_server/session.rs
index a431d23c..40110588 100644
--- a/src/client_server/session.rs
+++ b/src/client_server/session.rs
@@ -12,14 +12,28 @@ use ruma::{
#[cfg(feature = "conduit_bin")]
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"))]
-pub fn get_login_route() -> ConduitResult {
+pub fn get_login_types_route() -> ConduitResult {
Ok(get_login_types::Response {
flows: vec![get_login_types::LoginType::Password],
}
.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(
feature = "conduit_bin",
post("/_matrix/client/r0/login", data = "")
@@ -74,6 +88,7 @@ pub fn login_route(
// Generate a new token for the device
let token = utils::random_string(TOKEN_LENGTH);
+ // TODO: Don't always create a new device
// Add device
db.users.create_device(
&user_id,
@@ -92,6 +107,12 @@ pub fn login_route(
.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(
feature = "conduit_bin",
post("/_matrix/client/r0/logout", data = "")
@@ -108,6 +129,15 @@ pub fn logout_route(
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(
feature = "conduit_bin",
post("/_matrix/client/r0/logout/all", data = "")
diff --git a/src/client_server/sync.rs b/src/client_server/sync.rs
index 4e670ec6..2307f028 100644
--- a/src/client_server/sync.rs
+++ b/src/client_server/sync.rs
@@ -2,17 +2,29 @@ use super::State;
use crate::{ConduitResult, Database, Error, Ruma};
use ruma::{
api::client::r0::sync::sync_events,
- events::{AnySyncEphemeralRoomEvent, EventType},
- Raw,
+ events::{room::member::MembershipState, AnySyncEphemeralRoomEvent, EventType},
+ Raw, RoomId, UserId,
};
#[cfg(feature = "conduit_bin")]
use rocket::{get, tokio};
use std::{
collections::{hash_map, BTreeMap, HashMap, HashSet},
+ convert::TryFrom,
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(
feature = "conduit_bin",
get("/_matrix/client/r0/sync", data = "")
@@ -40,7 +52,9 @@ pub async fn sync_events_route(
.unwrap_or(0);
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_left = HashSet::new();
// Look for device list updates of this account
device_list_updates.extend(
@@ -67,46 +81,100 @@ pub async fn sync_events_route(
.rev()
.collect::>();
+ let send_notification_counts = !timeline_pdus.is_empty();
+
// They /sync response doesn't always return all messages, so we say the output is
// limited unless there are events in non_timeline_pdus
- //let mut limited = false;
+ let mut limited = false;
let mut state_pdus = Vec::new();
for pdu in non_timeline_pdus {
if pdu.state_key.is_some() {
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 joined_since_last_sync = false;
- let mut send_notification_counts = false;
- for pdu in db
+ let mut new_encrypted_room = false;
+ for (state_key, pdu) in db
.rooms
.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 {
send_member_count = true;
- if !joined_since_last_sync && pdu.state_key == Some(sender_id.to_string()) {
- let content = serde_json::from_value::<
- Raw,
- >(pdu.content.clone())
- .expect("Raw::from_value always works")
- .deserialize()
- .map_err(|_| Error::bad_database("Invalid PDU in database."))?;
- if content.membership == ruma::events::room::member::MembershipState::Join {
- joined_since_last_sync = true;
- // Both send_member_count and joined_since_last_sync are set. There's
- // nothing more to do
- break;
+
+ let content = serde_json::from_value::<
+ Raw,
+ >(pdu.content.clone())
+ .expect("Raw::from_value always works")
+ .deserialize()
+ .map_err(|_| Error::bad_database("Invalid PDU in database."))?;
+
+ if pdu.state_key == Some(sender_id.to_string())
+ && content.membership == MembershipState::Join
+ {
+ 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 = 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."))?;
if let Some(state_key) = &pdu.state_key {
- let current_content = serde_json::from_value::<
- Raw,
- >(
- 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.")
+ let user_id = UserId::try_from(state_key.clone()).map_err(|_| {
+ Error::bad_database("Invalid UserId in member PDU.")
})?;
// The membership was and still is invite or join
if matches!(
content.membership,
- ruma::events::room::member::MembershipState::Join
- | ruma::events::room::member::MembershipState::Invite
- ) && matches!(
- current_content.membership,
- ruma::events::room::member::MembershipState::Join
- | ruma::events::room::member::MembershipState::Invite
- ) {
+ MembershipState::Join | MembershipState::Invite
+ ) && (db.rooms.is_joined(&user_id, &room_id)?
+ || db.rooms.is_invited(&user_id, &room_id)?)
+ {
Ok::<_, Error>(Some(state_key.clone()))
} else {
Ok(None)
@@ -274,7 +324,7 @@ pub async fn sync_events_route(
notification_count,
},
timeline: sync_events::Timeline {
- limited: joined_since_last_sync,
+ limited: limited || joined_since_last_sync,
prev_batch,
events: room_events,
},
@@ -297,13 +347,6 @@ pub async fn sync_events_route(
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
for (user_id, presence) in
db.rooms
@@ -348,31 +391,6 @@ pub async fn sync_events_route(
.map(|pdu| pdu.to_sync_room_event())
.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::>();
-
- 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 {
account_data: sync_events::AccountData { events: Vec::new() },
timeline: sync_events::Timeline {
@@ -383,6 +401,49 @@ pub async fn sync_events_route(
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,
+ >(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() {
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) {
let room_id = room_id?;
let mut invited_since_last_sync = false;
- for pdu in db
- .rooms
- .pdus_since(&sender_id, &room_id, since)?
- {
+ for pdu in db.rooms.pdus_since(&sender_id, &room_id, since)? {
let pdu = pdu?;
- if pdu.kind == EventType::RoomMember {
- if pdu.state_key == Some(sender_id.to_string()) {
- let content = serde_json::from_value::<
- Raw,
- >(pdu.content.clone())
- .expect("Raw::from_value always works")
- .deserialize()
- .map_err(|_| Error::bad_database("Invalid PDU in database."))?;
- if content.membership == ruma::events::room::member::MembershipState::Invite {
- invited_since_last_sync = true;
- break;
- }
+ if pdu.kind == EventType::RoomMember && pdu.state_key == Some(sender_id.to_string()) {
+ let content = serde_json::from_value::<
+ Raw,
+ >(pdu.content.clone())
+ .expect("Raw::from_value always works")
+ .deserialize()
+ .map_err(|_| Error::bad_database("Invalid PDU in database."))?;
+
+ if content.membership == MembershipState::Invite {
+ invited_since_last_sync = true;
+ 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*
db.users
.remove_to_device_events(sender_id, device_id, since)?;
@@ -464,7 +542,7 @@ pub async fn sync_events_route(
},
device_lists: sync_events::DeviceLists {
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 {
db.users.count_one_time_keys(sender_id, device_id)?
@@ -500,3 +578,24 @@ pub async fn sync_events_route(
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)
+}
diff --git a/src/client_server/unversioned.rs b/src/client_server/unversioned.rs
index e71c194c..3ff8bec2 100644
--- a/src/client_server/unversioned.rs
+++ b/src/client_server/unversioned.rs
@@ -5,6 +5,16 @@ use std::collections::BTreeMap;
#[cfg(feature = "conduit_bin")]
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"))]
pub fn get_supported_versions_route() -> ConduitResult {
let mut unstable_features = BTreeMap::new();
diff --git a/src/database.rs b/src/database.rs
index 844a1f47..7bbb6dd7 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -104,6 +104,8 @@ impl Database {
aliasid_alias: db.open_tree("alias_roomid")?,
publicroomids: db.open_tree("publicroomids")?,
+ tokenids: db.open_tree("tokenids")?,
+
userroomid_joined: db.open_tree("userroomid_joined")?,
roomuserid_joined: db.open_tree("roomuserid_joined")?,
userroomid_invited: db.open_tree("userroomid_invited")?,
@@ -127,7 +129,6 @@ impl Database {
pub async fn watch(&self, user_id: &UserId, device_id: &DeviceId) {
let userid_bytes = user_id.to_string().as_bytes().to_vec();
-
let mut userid_prefix = userid_bytes.clone();
userid_prefix.push(0xff);
@@ -151,7 +152,8 @@ impl Database {
// Events for rooms we are in
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);
// PDUs
@@ -162,7 +164,7 @@ impl Database {
self.rooms
.edus
.roomid_lastroomactiveupdate
- .watch_prefix(&roomid_prefix),
+ .watch_prefix(&roomid_bytes),
);
futures.push(
diff --git a/src/database/rooms.rs b/src/database/rooms.rs
index fe633180..d2cd5e9f 100644
--- a/src/database/rooms.rs
+++ b/src/database/rooms.rs
@@ -35,6 +35,8 @@ pub struct Rooms {
pub(super) aliasid_alias: sled::Tree, // AliasId = RoomId + Count
pub(super) publicroomids: sled::Tree,
+ pub(super) tokenids: sled::Tree, // TokenId = RoomId + Token + PduId
+
pub(super) userroomid_joined: sled::Tree,
pub(super) roomuserid_joined: 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.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 {
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)?;
@@ -928,6 +945,95 @@ impl Rooms {
})
}
+ pub fn search_pdus<'a>(
+ &'a self,
+ room_id: &RoomId,
+ search_string: &str,
+ ) -> Result<(impl Iterator- + 'a, Vec)> {
+ 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::>();
+
+ 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,
+ ) -> impl Iterator
- > + '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.
pub fn room_members(&self, room_id: &RoomId) -> impl Iterator
- > {
self.roomuserid_joined
diff --git a/src/database/users.rs b/src/database/users.rs
index 594cc2d1..1b6a6812 100644
--- a/src/database/users.rs
+++ b/src/database/users.rs
@@ -408,19 +408,7 @@ impl Users {
&*serde_json::to_string(&device_keys).expect("DeviceKeys::to_string always works"),
)?;
- let count = globals.next_count()?.to_be_bytes();
- 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())?;
+ self.mark_device_key_update(user_id, rooms, globals)?;
Ok(())
}
@@ -520,19 +508,7 @@ impl Users {
.insert(&*user_id.to_string(), user_signing_key_key)?;
}
- let count = globals.next_count()?.to_be_bytes();
- 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())?;
+ self.mark_device_key_update(user_id, rooms, globals)?;
Ok(())
}
@@ -576,21 +552,7 @@ impl Users {
)?;
// TODO: Should we notify about this change?
- let count = globals.next_count()?.to_be_bytes();
- 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())?;
+ self.mark_device_key_update(target_id, rooms, globals)?;
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(
&self,
user_id: &UserId,
@@ -859,7 +852,9 @@ impl Users {
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(), "")?;
// TODO: Unhook 3PID
diff --git a/src/error.rs b/src/error.rs
index af5405c2..623aa0ef 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -27,6 +27,13 @@ pub enum Error {
#[from]
source: image::error::ImageError,
},
+ #[error("Could not connect to server.")]
+ ReqwestError {
+ #[from]
+ source: reqwest::Error,
+ },
+ #[error("{0}")]
+ BadServerResponse(&'static str),
#[error("{0}")]
BadConfig(&'static str),
#[error("{0}")]
diff --git a/src/lib.rs b/src/lib.rs
index 96236bfc..eea32c75 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,6 +4,7 @@ mod error;
mod pdu;
mod push_rules;
mod ruma_wrapper;
+pub mod server_server;
mod utils;
pub use database::Database;
diff --git a/src/main.rs b/src/main.rs
index f91a10f3..bbe7c962 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,13 +1,13 @@
#![warn(rust_2018_idioms)]
-pub mod push_rules;
+pub mod client_server;
+pub mod server_server;
-mod client_server;
mod database;
mod error;
mod pdu;
+mod push_rules;
mod ruma_wrapper;
-//mod server_server;
mod utils;
pub use database::Database;
@@ -26,7 +26,7 @@ fn setup_rocket() -> rocket::Rocket {
client_server::get_supported_versions_route,
client_server::get_register_available_route,
client_server::register_route,
- client_server::get_login_route,
+ client_server::get_login_types_route,
client_server::login_route,
client_server::whoami_route,
client_server::logout_route,
@@ -90,6 +90,7 @@ fn setup_rocket() -> rocket::Rocket {
client_server::sync_events_route,
client_server::get_context_route,
client_server::get_message_events_route,
+ client_server::search_events_route,
client_server::turn_server_route,
client_server::send_event_to_device_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_pushers_route,
client_server::set_pushers_route,
- //server_server::well_known_server,
- //server_server::get_server_version,
- //server_server::get_server_keys,
- //server_server::get_server_keys_deprecated,
+ server_server::well_known_server,
+ server_server::get_server_version,
+ server_server::get_server_keys,
+ 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 {
diff --git a/src/pdu.rs b/src/pdu.rs
index b689a3ee..9936802e 100644
--- a/src/pdu.rs
+++ b/src/pdu.rs
@@ -37,11 +37,12 @@ pub struct PduEvent {
impl PduEvent {
pub fn redact(&mut self) -> Result<()> {
self.unsigned.clear();
- let allowed = match self.kind {
- EventType::RoomMember => vec!["membership"],
- EventType::RoomCreate => vec!["creator"],
- EventType::RoomJoinRules => vec!["join_rule"],
- EventType::RoomPowerLevels => vec![
+
+ let allowed: &[&str] = match self.kind {
+ EventType::RoomMember => &["membership"],
+ EventType::RoomCreate => &["creator"],
+ EventType::RoomJoinRules => &["join_rule"],
+ EventType::RoomPowerLevels => &[
"ban",
"events",
"events_default",
@@ -51,8 +52,8 @@ impl PduEvent {
"users",
"users_default",
],
- EventType::RoomHistoryVisibility => vec!["history_visibility"],
- _ => vec![],
+ EventType::RoomHistoryVisibility => &["history_visibility"],
+ _ => &[],
};
let old_content = self
@@ -63,8 +64,8 @@ impl PduEvent {
let mut new_content = serde_json::Map::new();
for key in allowed {
- if let Some(value) = old_content.remove(key) {
- new_content.insert(key.to_owned(), value);
+ if let Some(value) = old_content.remove(*key) {
+ new_content.insert((*key).to_owned(), value);
}
}
diff --git a/src/server_server.rs b/src/server_server.rs
index a2141433..f48f502b 100644
--- a/src/server_server.rs
+++ b/src/server_server.rs
@@ -1,16 +1,19 @@
-use crate::{Database, MatrixResult};
+use crate::{client_server, ConduitResult, Database, Error, Result, Ruma};
use http::header::{HeaderValue, AUTHORIZATION};
-use log::error;
-use rocket::{get, response::content::Json, State};
-use ruma::api::Endpoint;
-use ruma::api::client::error::Error;
-use ruma::api::federation::discovery::{
- get_server_keys::v2 as get_server_keys, get_server_version::v1 as get_server_version,
+use rocket::{get, post, put, response::content::Json, State};
+use ruma::api::federation::{
+ directory::get_public_rooms,
+ discovery::{
+ get_server_keys, get_server_version::v1 as get_server_version, ServerKey, VerifyKey,
+ },
+ transactions::send_transaction_message,
};
+use ruma::api::{client, OutgoingRequest};
use serde_json::json;
use std::{
collections::BTreeMap,
convert::TryFrom,
+ fmt::Debug,
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())
}
-pub async fn send_request(
+pub async fn send_request(
db: &crate::Database,
destination: String,
request: T,
-) -> Option {
- let mut http_request: http::Request<_> = request.try_into().unwrap();
-
+) -> Result
+where
+ T: Debug,
+{
let actual_destination = "https://".to_owned()
+ &request_well_known(db, &destination)
.await
.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();
if !http_request.body().is_empty() {
request_map.insert(
"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("uri".to_owned(), T::METADATA.path.into());
- request_map.insert("origin".to_owned(), db.globals.server_name().into());
+ request_map.insert(
+ "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());
let mut request_json = request_map.into();
ruma::signatures::sign_json(
- db.globals.server_name(),
+ db.globals.server_name().as_str(),
db.globals.keypair(),
&mut request_json,
)
@@ -72,31 +90,32 @@ pub async fn send_request(
.as_object()
.unwrap()
.values()
- .next()
- .unwrap()
- .as_object()
- .unwrap()
- .iter()
- .map(|(k, v)| (k, v.as_str().unwrap()));
+ .map(|v| {
+ v.as_object()
+ .unwrap()
+ .iter()
+ .map(|(k, v)| (k, v.as_str().unwrap()))
+ });
- for s in signatures {
- http_request.headers_mut().insert(
- AUTHORIZATION,
- HeaderValue::from_str(&format!(
- "X-Matrix origin={},key=\"{}\",sig=\"{}\"",
- db.globals.server_name(),
- s.0,
- s.1
- ))
- .unwrap(),
- );
+ for signature_server in signatures {
+ for s in signature_server {
+ http_request.headers_mut().insert(
+ AUTHORIZATION,
+ HeaderValue::from_str(&format!(
+ "X-Matrix origin={},key=\"{}\",sig=\"{}\"",
+ db.globals.server_name(),
+ s.0,
+ s.1
+ ))
+ .unwrap(),
+ );
+ }
}
- let reqwest_response = db
- .globals
- .reqwest_client()
- .execute(http_request.into())
- .await;
+ let reqwest_request = reqwest::Request::try_from(http_request)
+ .expect("all http requests are valid reqwest requests");
+
+ let reqwest_response = db.globals.reqwest_client().execute(reqwest_request).await;
// Because reqwest::Response -> http::Response is complicated:
match reqwest_response {
@@ -117,59 +136,56 @@ pub async fn send_request(
.unwrap()
.into_iter()
.collect();
- Some(
- ::try_from(http_response.body(body).unwrap())
- .ok()
- .unwrap(),
+ Ok(
+ T::IncomingResponse::try_from(http_response.body(body).unwrap())
+ .expect("TODO: error handle other server errors"),
)
}
- Err(e) => {
- error!("{}", e);
- None
- }
+ Err(e) => Err(e.into()),
}
}
-#[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 {
- rocket::response::content::Json(
- json!({ "m.server": "matrixtesting.koesters.xyz:14004"}).to_string(),
- )
+ rocket::response::content::Json(json!({ "m.server": "pc.koesters.xyz:59003"}).to_string())
}
-#[cfg_attr(feature = "conduit_bin",get("/_matrix/federation/v1/version"))]
-pub fn get_server_version() -> MatrixResult {
- MatrixResult(Ok(get_server_version::Response {
+#[cfg_attr(feature = "conduit_bin", get("/_matrix/federation/v1/version"))]
+pub fn get_server_version() -> ConduitResult {
+ Ok(get_server_version::Response {
server: Some(get_server_version::Server {
name: Some("Conduit".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 {
let mut verify_keys = BTreeMap::new();
verify_keys.insert(
format!("ed25519:{}", db.globals.keypair().version()),
- get_server_keys::VerifyKey {
+ VerifyKey {
key: base64::encode_config(db.globals.keypair().public_key(), base64::STANDARD_NO_PAD),
},
);
let mut response = serde_json::from_slice(
- http::Response::try_from(get_server_keys::Response {
- server_name: db.globals.server_name().to_owned(),
- verify_keys,
- old_verify_keys: BTreeMap::new(),
- signatures: BTreeMap::new(),
- valid_until_ts: SystemTime::now() + Duration::from_secs(60 * 2),
+ http::Response::try_from(get_server_keys::v2::Response {
+ server_key: ServerKey {
+ server_name: db.globals.server_name().to_owned(),
+ verify_keys,
+ old_verify_keys: BTreeMap::new(),
+ signatures: BTreeMap::new(),
+ valid_until_ts: SystemTime::now() + Duration::from_secs(60 * 2),
+ },
})
.unwrap()
.body(),
)
.unwrap();
ruma::signatures::sign_json(
- db.globals.server_name(),
+ db.globals.server_name().as_str(),
db.globals.keypair(),
&mut response,
)
@@ -177,7 +193,88 @@ pub fn get_server_keys(db: State<'_, Database>) -> Json {
Json(response.to_string())
}
-#[cfg_attr(feature = "conduit_bin",get("/_matrix/key/v2/server/<_key_id>"))]
-pub fn get_server_keys_deprecated(db: State<'_, Database>, _key_id: String) -> Json {
+#[cfg_attr(feature = "conduit_bin", get("/_matrix/key/v2/server/<_>"))]
+pub fn get_server_keys_deprecated(db: State<'_, Database>) -> Json {
get_server_keys(db)
}
+
+#[cfg_attr(
+ feature = "conduit_bin",
+ post("/_matrix/federation/v1/publicRooms", data = "")
+)]
+pub async fn get_public_rooms_route(
+ db: State<'_, Database>,
+ body: Ruma,
+) -> ConduitResult {
+ 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 = "")
+)]
+pub fn send_transaction_message_route(
+ db: State<'_, Database>,
+ body: Ruma,
+) -> ConduitResult {
+ dbg!(&*body);
+ Ok(send_transaction_message::v1::Response {
+ pdus: BTreeMap::new(),
+ }
+ .into())
+}
diff --git a/src/utils.rs b/src/utils.rs
index 0ab3bfab..8cf1b2ce 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,6 +1,9 @@
use argon2::{Config, Variant};
+use cmp::Ordering;
use rand::prelude::*;
+use sled::IVec;
use std::{
+ cmp,
convert::TryInto,
time::{SystemTime, UNIX_EPOCH},
};
@@ -59,3 +62,31 @@ pub fn calculate_hash(password: &str) -> Result {
let salt = random_string(32);
argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &hashing_config)
}
+
+pub fn common_elements(
+ mut iterators: impl Iterator
- >,
+ check_order: impl Fn(&IVec, &IVec) -> Ordering,
+) -> Option> {
+ let first_iterator = iterators.next()?;
+ let mut other_iterators = iterators.map(|i| i.peekable()).collect::>();
+
+ 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)
+ }))
+}