From abcce95dd8eadcef233b96e082b865fa3cf764f3 Mon Sep 17 00:00:00 2001 From: timokoesters Date: Tue, 14 Apr 2020 13:54:32 +0200 Subject: [PATCH] feat: invites, better public room dir, user search --- README.md | 1 + src/client_server.rs | 164 +++++++++++++++++++++++++++++++++++-------- src/data.rs | 82 ++++++++++++++++++++-- src/database.rs | 32 +++++++-- src/main.rs | 4 ++ src/pdu.rs | 16 ++++- 6 files changed, 257 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 169a1df..b2b42b0 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ A fast Matrix homeserver that's optimized for smaller, personal servers, instead - [x] Riot room discovery - [x] Riot read receipts - [x] Typing indications +- [x] Invites, user search - [ ] Riot presence - [ ] Password hashing - [ ] Proper room creation diff --git a/src/client_server.rs b/src/client_server.rs index 660da6c..f1c8a85 100644 --- a/src/client_server.rs +++ b/src/client_server.rs @@ -1,7 +1,7 @@ -use crate::{utils, Data, Database, MatrixResult, PduEvent, Ruma}; +use crate::{utils, Data, MatrixResult, Ruma}; use log::debug; -use rocket::{get, options, post, put, routes, State}; +use rocket::{get, options, post, put, State}; use ruma_client_api::{ error::{Error, ErrorKind}, r0::{ @@ -14,7 +14,7 @@ use ruma_client_api::{ directory::{self, get_public_rooms_filtered}, filter::{self, create_filter, get_filter}, keys::{get_keys, upload_keys}, - membership::{join_room_by_id, join_room_by_id_or_alias}, + membership::{invite_user, join_room_by_id, join_room_by_id_or_alias}, message::create_message_event, presence::set_presence, profile::{ @@ -28,21 +28,14 @@ use ruma_client_api::{ sync::sync_events, thirdparty::get_protocols, typing::create_typing_event, + user_directory::search_users, }, unversioned::get_supported_versions, }; -use ruma_events::{ - collections::only::{Event as EduEvent, Event}, - EventType, -}; +use ruma_events::{collections::only::Event as EduEvent, EventType}; use ruma_identifiers::{RoomId, RoomIdOrAliasId, UserId}; use serde_json::json; -use std::{ - collections::HashMap, - convert::{TryFrom, TryInto}, - path::PathBuf, - time::Duration, -}; +use std::{collections::HashMap, convert::TryInto, path::PathBuf, time::Duration}; const GUEST_NAME_LENGTH: usize = 10; const DEVICE_ID_LENGTH: usize = 10; @@ -133,10 +126,8 @@ pub fn register_route( })) } -#[get("/_matrix/client/r0/login", data = "<_body>")] -pub fn get_login_route( - _body: Ruma, -) -> MatrixResult { +#[get("/_matrix/client/r0/login")] +pub fn get_login_route() -> MatrixResult { MatrixResult(Ok(get_login_types::Response { flows: vec![get_login_types::LoginType::Password], })) @@ -451,6 +442,7 @@ pub fn upload_keys_route( data: State, body: Ruma, ) -> MatrixResult { + // TODO MatrixResult(Ok(upload_keys::Response { one_time_key_counts: HashMap::new(), })) @@ -542,6 +534,24 @@ pub fn create_room_route( Some("".to_owned()), ); + data.pdu_append( + room_id.clone(), + user_id.clone(), + EventType::RoomPowerLevels, + json!({ + "ban": 50, + "events_default": 0, + "invite": 50, + "kick": 50, + "redact": 50, + "state_default": 50, + "users": { user_id.to_string(): 100 }, + "users_default": 0 + }), + None, + Some("".to_owned()), + ); + if let Some(name) = &body.name { data.pdu_append( room_id.clone(), @@ -564,8 +574,14 @@ pub fn create_room_route( ); } + dbg!(&*body); + data.room_join(&room_id, &user_id); + for user in &body.invite { + data.room_invite(&user_id, &room_id, user); + } + MatrixResult(Ok(create_room::Response { room_id })) } @@ -650,27 +666,58 @@ pub fn join_room_by_id_or_alias_route( } } +#[post("/_matrix/client/r0/rooms/<_room_id>/invite", data = "")] +pub fn invite_user_route( + data: State, + body: Ruma, + _room_id: String, +) -> MatrixResult { + if let invite_user::InvitationRecipient::UserId { user_id } = &body.recipient { + data.room_invite( + &body.user_id.as_ref().expect("user is authenticated"), + &body.room_id, + &user_id, + ); + MatrixResult(Ok(invite_user::Response)) + } else { + MatrixResult(Err(Error { + kind: ErrorKind::NotFound, + message: "User not found.".to_owned(), + status_code: http::StatusCode::NOT_FOUND, + })) + } +} + #[post("/_matrix/client/r0/publicRooms", data = "")] pub fn get_public_rooms_filtered_route( data: State, body: Ruma, ) -> MatrixResult { - let chunk = data + let mut chunk = data .rooms_all() .into_iter() - .map(|room_id| directory::PublicRoomsChunk { - aliases: None, - canonical_alias: None, - name: None, - num_joined_members: data.room_users(&room_id).into(), - room_id, - topic: None, - world_readable: false, - guest_can_join: true, - avatar_url: None, + .map(|room_id| { + let state = data.room_state(&room_id); + directory::PublicRoomsChunk { + aliases: None, + canonical_alias: None, + name: state + .get(&(EventType::RoomName, "".to_owned())) + .and_then(|s| s.content.get("name")) + .and_then(|n| n.as_str()) + .map(|n| n.to_owned()), + num_joined_members: data.room_users(&room_id).into(), + room_id, + topic: None, + world_readable: false, + guest_can_join: true, + avatar_url: None, + } }) .collect::>(); + chunk.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members)); + let total_room_count_estimate = (chunk.len() as u32).into(); MatrixResult(Ok(get_public_rooms_filtered::Response { @@ -681,10 +728,31 @@ pub fn get_public_rooms_filtered_route( })) } +#[post("/_matrix/client/r0/user_directory/search", data = "")] +pub fn search_users_route( + data: State, + body: Ruma, +) -> MatrixResult { + MatrixResult(Ok(search_users::Response { + results: data + .users_all() + .into_iter() + .filter(|user_id| user_id.to_string().contains(&body.search_term)) + .map(|user_id| search_users::User { + user_id, + display_name: None, + avatar_url: None, + }) + .collect(), + limited: false, + })) +} + #[get("/_matrix/client/r0/thirdparty/protocols", data = "")] pub fn get_protocols_route( body: Ruma, ) -> MatrixResult { + // TODO MatrixResult(Ok(get_protocols::Response { protocols: HashMap::new(), })) @@ -776,7 +844,7 @@ pub fn sync_route( .and_then(|string| string.parse().ok()) .unwrap_or(0); for room_id in joined_roomids { - let pdus = { data.pdus_since(&room_id, since) }; + let pdus = data.pdus_since(&room_id, since); let room_events = pdus.into_iter().map(|pdu| pdu.to_room_event()).collect(); let mut edus = data.roomlatests_since(&room_id, since); edus.extend_from_slice(&data.roomactives_in(&room_id)); @@ -805,12 +873,28 @@ pub fn sync_route( ); } + let mut invited_rooms = HashMap::new(); + for room_id in data.rooms_invited(body.user_id.as_ref().expect("user is authenticated")) { + let events = data + .pdus_since(&room_id, since) + .into_iter() + .filter_map(|pdu| pdu.to_stripped_state_event()) + .collect(); + + invited_rooms.insert( + room_id, + sync_events::InvitedRoom { + invite_state: sync_events::InviteState { events }, + }, + ); + } + MatrixResult(Ok(sync_events::Response { next_batch, rooms: sync_events::Rooms { leave: Default::default(), join: joined_rooms, - invite: Default::default(), + invite: invited_rooms, }, presence: sync_events::Presence { events: Vec::new() }, device_lists: Default::default(), @@ -819,6 +903,26 @@ pub fn sync_route( })) } +#[get("/_matrix/client/r0/voip/turnServer")] +pub fn turn_server_route() -> MatrixResult { + // TODO + MatrixResult(Err(Error { + kind: ErrorKind::NotFound, + message: "There is no turn server yet.".to_owned(), + status_code: http::StatusCode::NOT_FOUND, + })) +} + +#[post("/_matrix/client/r0/publicised_groups")] +pub fn publicised_groups_route() -> MatrixResult { + // TODO + MatrixResult(Err(Error { + kind: ErrorKind::NotFound, + message: "There are no publicised groups yet.".to_owned(), + status_code: http::StatusCode::NOT_FOUND, + })) +} + #[options("/<_segments..>")] pub fn options_route(_segments: PathBuf) -> MatrixResult { MatrixResult(Err(Error { diff --git a/src/data.rs b/src/data.rs index 85909f8..815586f 100644 --- a/src/data.rs +++ b/src/data.rs @@ -52,6 +52,15 @@ impl Data { .and_then(|bytes| (*utils::string_from_bytes(&bytes)).try_into().ok()) } + pub fn users_all(&self) -> Vec { + self.db + .userid_password + .iter() + .keys() + .map(|k| UserId::try_from(&*utils::string_from_bytes(&k.unwrap())).unwrap()) + .collect() + } + /// Checks if the given password is equal to the one in the database. pub fn password_get(&self, user_id: &UserId) -> Option { self.db @@ -139,13 +148,16 @@ impl Data { .any(|device| device == device_id.as_bytes())); // Does the user have that device? // Remove old token - if let Some(old_token) = self.db.deviceid_token.get(device_id).unwrap() { + let mut key = user_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(device_id.as_bytes()); + if let Some(old_token) = self.db.userdeviceid_token.get(&key).unwrap() { self.db.token_userid.remove(old_token).unwrap(); // It will be removed from deviceid_token by the insert later } // Assign token to device_id - self.db.deviceid_token.insert(device_id, &*token).unwrap(); + self.db.userdeviceid_token.insert(key, &*token).unwrap(); // Assign token to user self.db @@ -167,6 +179,10 @@ impl Data { room_id.to_string().as_bytes(), user_id.to_string().as_bytes().into(), ); + self.db.userid_inviteroomids.remove_value( + user_id.to_string().as_bytes(), + room_id.to_string().as_bytes(), + ); self.pdu_append( room_id.clone(), @@ -239,6 +255,52 @@ impl Data { .count() as u32 } + pub fn room_state(&self, room_id: &RoomId) -> HashMap<(EventType, String), PduEvent> { + let mut hashmap = HashMap::new(); + for pdu in self + .db + .roomstateid_pdu + .scan_prefix(&room_id.to_string().as_bytes()) + .values() + .map(|value| serde_json::from_slice::(&value.unwrap()).unwrap()) + { + hashmap.insert( + ( + pdu.kind.clone(), + pdu.state_key + .clone() + .expect("state events have a state key"), + ), + pdu, + ); + } + hashmap + } + + pub fn room_invite(&self, sender: &UserId, room_id: &RoomId, user_id: &UserId) { + self.pdu_append( + room_id.clone(), + sender.clone(), + EventType::RoomMember, + json!({"membership": "invite"}), + None, + Some(user_id.to_string()), + ); + self.db.userid_inviteroomids.add( + &user_id.to_string().as_bytes(), + room_id.to_string().as_bytes().into(), + ); + } + + pub fn rooms_invited(&self, user_id: &UserId) -> Vec { + self.db + .userid_inviteroomids + .get_iter(&user_id.to_string().as_bytes()) + .values() + .map(|key| RoomId::try_from(&*utils::string_from_bytes(&key.unwrap())).unwrap()) + .collect() + } + pub fn pdu_get(&self, event_id: &EventId) -> Option { self.db .eventid_pduid @@ -360,16 +422,24 @@ impl Data { pdu_id.push(0xff); // Add delimiter so we don't find rooms starting with the same id pdu_id.extend_from_slice(&index.to_be_bytes()); - self.db - .pduid_pdu - .insert(&pdu_id, &*serde_json::to_string(&pdu).unwrap()) - .unwrap(); + let pdu_json = serde_json::to_string(&pdu).unwrap(); + + self.db.pduid_pdu.insert(&pdu_id, &*pdu_json).unwrap(); self.db .eventid_pduid .insert(pdu.event_id.to_string(), pdu_id.clone()) .unwrap(); + if let Some(state_key) = pdu.state_key { + let mut key = room_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(dbg!(pdu.kind.to_string().as_bytes())); + key.push(0xff); + key.extend_from_slice(state_key.to_string().as_bytes()); + self.db.roomstateid_pdu.insert(key, &*pdu_json).unwrap(); + } + pdu.event_id } diff --git a/src/database.rs b/src/database.rs index c7a8623..041a215 100644 --- a/src/database.rs +++ b/src/database.rs @@ -27,6 +27,16 @@ impl MultiValue { } } + pub fn remove_value(&self, id: &[u8], value: &[u8]) { + if let Some(key) = self + .get_iter(id) + .find(|t| t.as_ref().unwrap().1 == value) + .map(|t| t.unwrap().0) + { + self.0.remove(key).unwrap(); + } + } + /// Add another value to the id. pub fn add(&self, id: &[u8], value: IVec) { // The new value will need a new index. We store the last used index in 'n' + id @@ -52,16 +62,18 @@ impl MultiValue { pub struct Database { pub userid_password: sled::Tree, - pub userid_deviceids: MultiValue, pub userid_displayname: sled::Tree, pub userid_avatarurl: sled::Tree, - pub deviceid_token: sled::Tree, + pub userid_deviceids: MultiValue, + pub userdeviceid_token: sled::Tree, pub token_userid: sled::Tree, pub pduid_pdu: sled::Tree, // PduId = 'd' + RoomId + Since (global since counter is at 'n') pub eventid_pduid: sled::Tree, pub roomid_pduleaves: MultiValue, + pub roomstateid_pdu: sled::Tree, // Room + StateType + StateKey pub roomid_userids: MultiValue, pub userid_roomids: MultiValue, + pub userid_inviteroomids: MultiValue, // EDUs: pub roomlatestid_roomlatest: sled::Tree, // Read Receipts, RoomLatestId = RoomId + Since + UserId TODO: Types pub roomactiveid_roomactive: sled::Tree, // Typing, RoomActiveId = TimeoutTime + Since @@ -95,13 +107,15 @@ impl Database { userid_deviceids: MultiValue(db.open_tree("userid_deviceids").unwrap()), userid_displayname: db.open_tree("userid_displayname").unwrap(), userid_avatarurl: db.open_tree("userid_avatarurl").unwrap(), - deviceid_token: db.open_tree("deviceid_token").unwrap(), + userdeviceid_token: db.open_tree("userdeviceid_token").unwrap(), token_userid: db.open_tree("token_userid").unwrap(), pduid_pdu: db.open_tree("pduid_pdu").unwrap(), eventid_pduid: db.open_tree("eventid_pduid").unwrap(), roomid_pduleaves: MultiValue(db.open_tree("roomid_pduleaves").unwrap()), + roomstateid_pdu: db.open_tree("roomstateid_pdu").unwrap(), roomid_userids: MultiValue(db.open_tree("roomid_userids").unwrap()), userid_roomids: MultiValue(db.open_tree("userid_roomids").unwrap()), + userid_inviteroomids: MultiValue(db.open_tree("userid_inviteroomids").unwrap()), roomlatestid_roomlatest: db.open_tree("roomlatestid_roomlatest").unwrap(), roomactiveid_roomactive: db.open_tree("roomactiveid_roomactive").unwrap(), globalallid_globalall: db.open_tree("globalallid_globalall").unwrap(), @@ -143,8 +157,8 @@ impl Database { String::from_utf8_lossy(&v), ); } - println!("\n# DeviceId -> Token:"); - for (k, v) in self.deviceid_token.iter().map(|r| r.unwrap()) { + println!("\n# UserId+DeviceId -> Token:"); + for (k, v) in self.userdeviceid_token.iter().map(|r| r.unwrap()) { println!( "{:?} -> {:?}", String::from_utf8_lossy(&k), @@ -167,6 +181,14 @@ impl Database { String::from_utf8_lossy(&v), ); } + println!("\n# RoomStateId -> PDU:"); + for (k, v) in self.roomstateid_pdu.iter().map(|r| r.unwrap()) { + println!( + "{:?} -> {:?}", + String::from_utf8_lossy(&k), + String::from_utf8_lossy(&v), + ); + } println!("\n# RoomId -> UserIds:"); for (k, v) in self.roomid_userids.iter_all().map(|r| r.unwrap()) { println!( diff --git a/src/main.rs b/src/main.rs index d09a787..f79a0b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,12 +45,16 @@ fn setup_rocket(data: Data) -> rocket::Rocket { client_server::get_alias_route, client_server::join_room_by_id_route, client_server::join_room_by_id_or_alias_route, + client_server::invite_user_route, client_server::get_public_rooms_filtered_route, + client_server::search_users_route, client_server::get_protocols_route, client_server::create_message_event_route, client_server::create_state_event_for_key_route, client_server::create_state_event_for_empty_key_route, client_server::sync_route, + client_server::turn_server_route, + client_server::publicised_groups_route, client_server::options_route, ], ) diff --git a/src/pdu.rs b/src/pdu.rs index 588242b..47f94ac 100644 --- a/src/pdu.rs +++ b/src/pdu.rs @@ -1,5 +1,7 @@ use js_int::UInt; -use ruma_events::{collections::all::RoomEvent, EventResult, EventType}; +use ruma_events::{ + collections::all::RoomEvent, stripped::AnyStrippedStateEvent, EventResult, EventType, +}; use ruma_federation_api::EventHash; use ruma_identifiers::{EventId, RoomId, UserId}; use serde::{Deserialize, Serialize}; @@ -39,4 +41,16 @@ impl PduEvent { .into_result() .unwrap() } + + pub fn to_stripped_state_event(&self) -> Option { + // Can only fail in rare circumstances that won't ever happen here, see + // https://docs.rs/serde_json/1.0.50/serde_json/fn.to_string.html + let json = serde_json::to_string(&self).unwrap(); + + // EventResult's deserialize implementation always returns `Ok(...)` + serde_json::from_str::>(&json) + .unwrap() + .into_result() + .ok() + } }