diff --git a/src/client_server/profile.rs b/src/client_server/profile.rs index 1313db776118e7b9d8c6c7813d96f3b04b858af4..ebcc7eb8f32b80a0359b852364a283bee9ae75fd 100644 --- a/src/client_server/profile.rs +++ b/src/client_server/profile.rs @@ -217,11 +217,8 @@ pub fn get_profile_route( db: State<'_, Database>, body: Ruma<get_profile::Request>, ) -> ConduitResult<get_profile::Response> { - let avatar_url = db.users.avatar_url(&body.user_id)?; - let displayname = db.users.displayname(&body.user_id)?; - - if avatar_url.is_none() && displayname.is_none() { - // Return 404 if we don't have a profile for this id + if !db.users.exists(&body.user_id)? { + // Return 404 if this user doesn't exist return Err(Error::BadRequest( ErrorKind::NotFound, "Profile was not found.", @@ -229,8 +226,8 @@ pub fn get_profile_route( } Ok(get_profile::Response { - avatar_url, - displayname, + avatar_url: db.users.avatar_url(&body.user_id)?, + displayname: db.users.displayname(&body.user_id)?, } .into()) } diff --git a/src/client_server/room.rs b/src/client_server/room.rs index b5f1529e592dd3d7f725c80e9167560bdd5a01b0..1b4387337a69399862c76bf1568175be39dfa6d3 100644 --- a/src/client_server/room.rs +++ b/src/client_server/room.rs @@ -3,15 +3,15 @@ use ruma::{ api::client::{ error::ErrorKind, - r0::room::{self, create_room, get_room_event}, + r0::room::{self, create_room, get_room_event, upgrade_room}, }, events::{ room::{guest_access, history_visibility, join_rules, member, name, topic}, EventType, }, - RoomAliasId, RoomId, RoomVersionId, + Raw, RoomAliasId, RoomId, RoomVersionId, }; -use std::{collections::BTreeMap, convert::TryFrom}; +use std::{cmp::max, collections::BTreeMap, convert::TryFrom}; #[cfg(feature = "conduit_bin")] use rocket::{get, post}; @@ -344,3 +344,196 @@ pub fn get_room_event_route( } .into()) } + +#[cfg_attr( + feature = "conduit_bin", + post("/_matrix/client/r0/rooms/<_room_id>/upgrade", data = "<body>") +)] +pub fn upgrade_room_route( + db: State<'_, Database>, + body: Ruma<upgrade_room::Request>, + _room_id: String, +) -> ConduitResult<upgrade_room::Response> { + let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + + // Validate the room version requested + let new_version = + RoomVersionId::try_from(body.new_version.clone()).expect("invalid room version id"); + + if !matches!( + new_version, + RoomVersionId::Version5 | RoomVersionId::Version6 + ) { + return Err(Error::BadRequest( + ErrorKind::UnsupportedRoomVersion, + "This server does not support that room version.", + )); + } + + // Create a replacement room + let replacement_room = RoomId::new(db.globals.server_name()); + + // Send a m.room.tombstone event to the old room to indicate that it is not intended to be used any further + // Fail if the sender does not have the required permissions + let tombstone_event_id = db.rooms.append_pdu( + PduBuilder { + room_id: body.room_id.clone(), + sender: sender_id.clone(), + event_type: EventType::RoomTombstone, + content: serde_json::to_value(ruma::events::room::tombstone::TombstoneEventContent { + body: "This room has been replaced".to_string(), + replacement_room: replacement_room.clone(), + }) + .expect("event is valid, we just created it"), + unsigned: None, + state_key: Some("".to_owned()), + redacts: None, + }, + &db.globals, + &db.account_data, + )?; + + // Get the old room federations status + let federate = serde_json::from_value::<Raw<ruma::events::room::create::CreateEventContent>>( + db.rooms + .room_state_get(&body.room_id, &EventType::RoomCreate, "")? + .ok_or_else(|| Error::bad_database("Found room without m.room.create event."))? + .content, + ) + .expect("Raw::from_value always works") + .deserialize() + .map_err(|_| Error::bad_database("Invalid room event in database."))? + .federate; + + // Use the m.room.tombstone event as the predecessor + let predecessor = Some(ruma::events::room::create::PreviousRoom::new( + body.room_id.clone(), + tombstone_event_id, + )); + + // Send a m.room.create event containing a predecessor field and the applicable room_version + let mut create_event_content = + ruma::events::room::create::CreateEventContent::new(sender_id.clone()); + create_event_content.federate = federate; + create_event_content.room_version = new_version; + create_event_content.predecessor = predecessor; + + db.rooms.append_pdu( + PduBuilder { + room_id: replacement_room.clone(), + sender: sender_id.clone(), + event_type: EventType::RoomCreate, + content: serde_json::to_value(create_event_content) + .expect("event is valid, we just created it"), + unsigned: None, + state_key: Some("".to_owned()), + redacts: None, + }, + &db.globals, + &db.account_data, + )?; + + // Join the new room + db.rooms.append_pdu( + PduBuilder { + room_id: replacement_room.clone(), + sender: sender_id.clone(), + event_type: EventType::RoomMember, + content: serde_json::to_value(member::MemberEventContent { + membership: member::MembershipState::Join, + displayname: db.users.displayname(&sender_id)?, + avatar_url: db.users.avatar_url(&sender_id)?, + is_direct: None, + third_party_invite: None, + }) + .expect("event is valid, we just created it"), + unsigned: None, + state_key: Some(sender_id.to_string()), + redacts: None, + }, + &db.globals, + &db.account_data, + )?; + + // Recommended transferable state events list from the specs + let transferable_state_events = vec![ + EventType::RoomServerAcl, + EventType::RoomEncryption, + EventType::RoomName, + EventType::RoomAvatar, + EventType::RoomTopic, + EventType::RoomGuestAccess, + EventType::RoomHistoryVisibility, + EventType::RoomJoinRules, + EventType::RoomPowerLevels, + ]; + + // Replicate transferable state events to the new room + for event_type in transferable_state_events { + let event_content = match db.rooms.room_state_get(&body.room_id, &event_type, "")? { + Some(v) => v.content.clone(), + None => continue, // Skipping missing events. + }; + + db.rooms.append_pdu( + PduBuilder { + room_id: replacement_room.clone(), + sender: sender_id.clone(), + event_type, + content: event_content, + unsigned: None, + state_key: Some("".to_owned()), + redacts: None, + }, + &db.globals, + &db.account_data, + )?; + } + + // Moves any local aliases to the new room + for alias in db.rooms.room_aliases(&body.room_id).filter_map(|r| r.ok()) { + db.rooms + .set_alias(&alias, Some(&replacement_room), &db.globals)?; + } + + // Get the old room power levels + let mut power_levels_event_content = + serde_json::from_value::<Raw<ruma::events::room::power_levels::PowerLevelsEventContent>>( + db.rooms + .room_state_get(&body.room_id, &EventType::RoomPowerLevels, "")? + .ok_or_else(|| Error::bad_database("Found room without m.room.create event."))? + .content, + ) + .expect("database contains invalid PDU") + .deserialize() + .map_err(|_| Error::bad_database("Invalid room event in database."))?; + + // Setting events_default and invite to the greater of 50 and users_default + 1 + let new_level = max( + 50.into(), + power_levels_event_content.users_default + 1.into(), + ); + power_levels_event_content.events_default = new_level; + power_levels_event_content.invite = new_level; + + // Modify the power levels in the old room to prevent sending of events and inviting new users + db.rooms + .append_pdu( + PduBuilder { + room_id: body.room_id.clone(), + sender: sender_id.clone(), + event_type: EventType::RoomPowerLevels, + content: serde_json::to_value(power_levels_event_content) + .expect("event is valid, we just created it"), + unsigned: None, + state_key: Some("".to_owned()), + redacts: None, + }, + &db.globals, + &db.account_data, + ) + .ok(); + + // Return the replacement room id + Ok(upgrade_room::Response { replacement_room }.into()) +} diff --git a/src/database.rs b/src/database.rs index b43cc5b03bff77a6c0b9df84d39f897d197ebc24..2bb75a58ba72ddbd08bfd047d43c04807a3528fc 100644 --- a/src/database.rs +++ b/src/database.rs @@ -112,6 +112,7 @@ pub fn load_or_create(config: &Config) -> Result<Self> { userroomid_joined: db.open_tree("userroomid_joined")?, roomuserid_joined: db.open_tree("roomuserid_joined")?, + roomuseroncejoinedids: db.open_tree("roomuseroncejoinedids")?, userroomid_invited: db.open_tree("userroomid_invited")?, roomuserid_invited: db.open_tree("roomuserid_invited")?, userroomid_left: db.open_tree("userroomid_left")?, diff --git a/src/database/rooms.rs b/src/database/rooms.rs index 8cfb61296f55c51d7180e56ee3039fc0eeacf854..22e61e6945bc35955f81263b41d71ae6e906157b 100644 --- a/src/database/rooms.rs +++ b/src/database/rooms.rs @@ -38,6 +38,7 @@ pub struct Rooms { pub(super) userroomid_joined: sled::Tree, pub(super) roomuserid_joined: sled::Tree, + pub(super) roomuseroncejoinedids: sled::Tree, pub(super) userroomid_invited: sled::Tree, pub(super) roomuserid_invited: sled::Tree, pub(super) userroomid_left: sled::Tree, @@ -782,6 +783,91 @@ fn update_membership( match &membership { member::MembershipState::Join => { + // Check if the user never joined this room + if !self.once_joined(&user_id, &room_id)? { + // Add the user ID to the join list then + self.roomuseroncejoinedids.insert(&userroom_id, &[])?; + + // Check if the room has a predecessor + if let Some(predecessor) = serde_json::from_value::< + Raw<ruma::events::room::create::CreateEventContent>, + >( + self.room_state_get(&room_id, &EventType::RoomCreate, "")? + .ok_or_else(|| { + Error::bad_database("Found room without m.room.create event.") + })? + .content, + ) + .expect("Raw::from_value always works") + .deserialize() + .map_err(|_| Error::bad_database("Invalid room event in database."))? + .predecessor + { + // Copy user settings from predecessor to the current room: + // - Push rules + // + // TODO: finish this once push rules are implemented. + // + // let mut push_rules_event_content = account_data + // .get::<ruma::events::push_rules::PushRulesEvent>( + // None, + // user_id, + // EventType::PushRules, + // )?; + // + // NOTE: find where `predecessor.room_id` match + // and update to `room_id`. + // + // account_data + // .update( + // None, + // user_id, + // EventType::PushRules, + // &push_rules_event_content, + // globals, + // ) + // .ok(); + + // Copy old tags to new room + if let Some(tag_event) = account_data.get::<ruma::events::tag::TagEvent>( + Some(&predecessor.room_id), + user_id, + EventType::Tag, + )? { + account_data + .update(Some(room_id), user_id, EventType::Tag, &tag_event, globals) + .ok(); + }; + + // Copy direct chat flag + if let Some(mut direct_event) = account_data + .get::<ruma::events::direct::DirectEvent>( + None, + user_id, + EventType::Direct, + )? { + let mut room_ids_updated = false; + + for room_ids in direct_event.content.0.values_mut() { + if room_ids.iter().any(|r| r == &predecessor.room_id) { + room_ids.push(room_id.clone()); + room_ids_updated = true; + } + } + + if room_ids_updated { + account_data.update( + None, + user_id, + EventType::Direct, + &direct_event, + globals, + )?; + } + }; + } + } + self.userroomid_joined.insert(&userroom_id, &[])?; self.roomuserid_joined.insert(&roomuser_id, &[])?; self.userroomid_invited.remove(&userroom_id)?; @@ -1042,6 +1128,27 @@ pub fn room_members(&self, room_id: &RoomId) -> impl Iterator<Item = Result<User }) } + /// Returns an iterator over all User IDs who ever joined a room. + pub fn room_useroncejoined(&self, room_id: &RoomId) -> impl Iterator<Item = Result<UserId>> { + self.roomuseroncejoinedids + .scan_prefix(room_id.to_string()) + .keys() + .map(|key| { + Ok(UserId::try_from( + utils::string_from_bytes( + &key? + .rsplit(|&b| b == 0xff) + .next() + .expect("rsplit always returns an element"), + ) + .map_err(|_| { + Error::bad_database("User ID in room_useroncejoined is invalid unicode.") + })?, + ) + .map_err(|_| Error::bad_database("User ID in room_useroncejoined is invalid."))?) + }) + } + /// Returns an iterator over all invited members of a room. pub fn room_members_invited(&self, room_id: &RoomId) -> impl Iterator<Item = Result<UserId>> { self.roomuserid_invited @@ -1126,6 +1233,14 @@ pub fn rooms_left(&self, user_id: &UserId) -> impl Iterator<Item = Result<RoomId }) } + pub fn once_joined(&self, user_id: &UserId, room_id: &RoomId) -> Result<bool> { + let mut userroom_id = user_id.to_string().as_bytes().to_vec(); + userroom_id.push(0xff); + userroom_id.extend_from_slice(room_id.to_string().as_bytes()); + + Ok(self.roomuseroncejoinedids.get(userroom_id)?.is_some()) + } + pub fn is_joined(&self, user_id: &UserId, room_id: &RoomId) -> Result<bool> { let mut userroom_id = user_id.to_string().as_bytes().to_vec(); userroom_id.push(0xff); diff --git a/src/main.rs b/src/main.rs index 96d0e99a4d5b3c99b493d5d0f102af594081d9be..eb060e3eca8f09b961a0bfae3baa11375e15da57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -118,6 +118,7 @@ fn setup_rocket() -> rocket::Rocket { client_server::get_key_changes_route, client_server::get_pushers_route, client_server::set_pushers_route, + client_server::upgrade_room_route, server_server::well_known_server, server_server::get_server_version, server_server::get_server_keys, diff --git a/sytest/sytest-whitelist b/sytest/sytest-whitelist index 158523307bcba5bfdf28fd8e3b25dbb35779549b..e1f4e5cd86494653e7608e64d441e07d25baa177 100644 --- a/sytest/sytest-whitelist +++ b/sytest/sytest-whitelist @@ -89,6 +89,7 @@ POST /rooms/:room_id/join can join a room POST /rooms/:room_id/leave can leave a room POST /rooms/:room_id/state/m.room.name sets name POST /rooms/:room_id/state/m.room.topic sets topic +POST /rooms/:room_id/upgrade can upgrade a room version POSTed media can be thumbnailed PUT /device/{deviceId} gives a 404 for unknown devices PUT /device/{deviceId} updates device fields