diff --git a/src/client_server/room.rs b/src/client_server/room.rs
index b5f1529e..1b438733 100644
--- a/src/client_server/room.rs
+++ b/src/client_server/room.rs
@@ -3,15 +3,15 @@ use crate::{pdu::PduBuilder, ConduitResult, Database, Error, Ruma};
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 = "
")
+)]
+pub fn upgrade_room_route(
+ db: State<'_, Database>,
+ body: Ruma,
+ _room_id: String,
+) -> ConduitResult {
+ 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::>(
+ 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::>(
+ 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 b43cc5b0..2bb75a58 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -112,6 +112,7 @@ impl Database {
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 8cfb6129..eee47f3a 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,104 @@ impl Rooms {
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,
+ >(
+ 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::(
+ // 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();
+
+ // - Tags
+ if let Some(basic_event) = account_data.get::(
+ Some(&predecessor.room_id),
+ user_id,
+ EventType::Tag,
+ )? {
+ let tag_event_content = basic_event.content;
+
+ account_data
+ .update(
+ Some(room_id),
+ user_id,
+ EventType::Tag,
+ &tag_event_content,
+ globals,
+ )
+ .ok();
+ };
+
+ // - Direct chat
+ if let Some(basic_event) = account_data
+ .get::(
+ None,
+ user_id,
+ EventType::Direct,
+ )?
+ {
+ let mut direct_event_content = basic_event.content;
+ 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_content,
+ globals,
+ )
+ .ok();
+ }
+ };
+ }
+ }
+
self.userroomid_joined.insert(&userroom_id, &[])?;
self.roomuserid_joined.insert(&roomuser_id, &[])?;
self.userroomid_invited.remove(&userroom_id)?;
@@ -1042,6 +1141,27 @@ impl Rooms {
})
}
+ /// Returns an iterator over all User IDs who ever joined a room.
+ pub fn room_useroncejoined(&self, room_id: &RoomId) -> impl Iterator- > {
+ 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
- > {
self.roomuserid_invited
@@ -1126,6 +1246,14 @@ impl Rooms {
})
}
+ pub fn once_joined(&self, user_id: &UserId, room_id: &RoomId) -> Result {
+ 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 {
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 96d0e99a..eb060e3e 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 15852330..e1f4e5cd 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