From 62bdfe1ce8e6400e3ac84a46eb1637a946c8f9d7 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Thu, 17 Jul 2025 23:15:14 +0100 Subject: [PATCH 1/8] feat(space-upgrades): Copy over space child & parent states --- src/api/client/room/upgrade.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/client/room/upgrade.rs b/src/api/client/room/upgrade.rs index ae632235..afd6f70e 100644 --- a/src/api/client/room/upgrade.rs +++ b/src/api/client/room/upgrade.rs @@ -24,7 +24,7 @@ use serde_json::{json, value::to_raw_value}; use crate::Ruma; /// Recommended transferable state events list from the spec -const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 9] = &[ +const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 11] = &[ StateEventType::RoomAvatar, StateEventType::RoomEncryption, StateEventType::RoomGuestAccess, @@ -34,6 +34,9 @@ const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 9] = &[ StateEventType::RoomPowerLevels, StateEventType::RoomServerAcl, StateEventType::RoomTopic, + // Not explicitly recommended in spec, but very useful. + StateEventType::SpaceChild, + StateEventType::SpaceParent, // TODO: m.room.policy ]; /// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade` @@ -50,10 +53,7 @@ pub(crate) async fn upgrade_room_route( State(services): State, body: Ruma, ) -> Result { - debug_assert!( - TRANSFERABLE_STATE_EVENTS.is_sorted(), - "TRANSFERABLE_STATE_EVENTS is not sorted" - ); + // TODO[v12]: Handle additional creators let sender_user = body.sender_user.as_ref().expect("user is authenticated"); if !services.server.supported_room_version(&body.new_version) { From b2883c3d6e43f736cc70959443cacd44c6a1b9c0 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 19 Jul 2025 15:08:21 +0100 Subject: [PATCH 2/8] feat(space-upgrades): Update parent spaces in upgrade This relies on the room being upgraded referencing the space itself, but there isn't an easy way to do it otherwise. --- src/api/client/room/upgrade.rs | 78 ++++++++++++++++++- .../rooms/state_accessor/room_state.rs | 19 +++++ src/service/rooms/state_accessor/user_can.rs | 32 +++++++- 3 files changed, 124 insertions(+), 5 deletions(-) diff --git a/src/api/client/room/upgrade.rs b/src/api/client/room/upgrade.rs index afd6f70e..3a0ed010 100644 --- a/src/api/client/room/upgrade.rs +++ b/src/api/client/room/upgrade.rs @@ -5,7 +5,7 @@ use conduwuit::{ Err, Error, Event, Result, err, info, matrix::{StateKey, pdu::PduBuilder}, }; -use futures::StreamExt; +use futures::{FutureExt, StreamExt}; use ruma::{ CanonicalJsonObject, RoomId, RoomVersionId, api::client::{error::ErrorKind, room::upgrade_room}, @@ -16,12 +16,13 @@ use ruma::{ power_levels::RoomPowerLevelsEventContent, tombstone::RoomTombstoneEventContent, }, + space::child::{RedactedSpaceChildEventContent, SpaceChildEventContent}, }, int, }; use serde_json::{json, value::to_raw_value}; -use crate::Ruma; +use crate::router::Ruma; /// Recommended transferable state events list from the spec const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 11] = &[ @@ -36,7 +37,7 @@ const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 11] = &[ StateEventType::RoomTopic, // Not explicitly recommended in spec, but very useful. StateEventType::SpaceChild, - StateEventType::SpaceParent, // TODO: m.room.policy + StateEventType::SpaceParent, // TODO: m.room.policy? ]; /// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade` @@ -128,7 +129,7 @@ pub(crate) async fn upgrade_room_route( ); }, | _ => { - // "creator" key no longer exists in V11+ rooms + // "creator" key no longer exists in V11 rooms create_event_content.remove("creator"); }, } @@ -175,6 +176,7 @@ pub(crate) async fn upgrade_room_route( &replacement_room, &state_lock, ) + .boxed() .await?; // Join the new room @@ -205,6 +207,7 @@ pub(crate) async fn upgrade_room_route( &replacement_room, &state_lock, ) + .boxed() .await?; // Replicate transferable state events to the new room @@ -233,6 +236,7 @@ pub(crate) async fn upgrade_room_route( &replacement_room, &state_lock, ) + .boxed() .await?; } @@ -290,10 +294,76 @@ pub(crate) async fn upgrade_room_route( &body.room_id, &state_lock, ) + .boxed() .await?; drop(state_lock); + // Check if the old room has a space parent, and if so, whether we should update + // it (m.space.parent, room_id) + let parents = services + .rooms + .state_accessor + .room_state_keys(&body.room_id, &StateEventType::SpaceParent) + .await?; + + for raw_space_id in parents { + let space_id = RoomId::parse(&raw_space_id)?; + let state_key = StateKey::from(raw_space_id.clone()); + let Ok(child) = services + .rooms + .state_accessor + .room_state_get_content::( + space_id, + &StateEventType::SpaceChild, + body.room_id.as_str(), + ) + .await + else { + // If the space does not have a child event for this room, we can skip it + continue; + }; + // First, drop the space's child event + let state_lock = services.rooms.state.mutex.lock(space_id).await; + services + .rooms + .timeline + .build_and_append_pdu( + PduBuilder { + event_type: StateEventType::SpaceChild.into(), + content: to_raw_value(&RedactedSpaceChildEventContent {}) + .expect("event is valid, we just created it"), + state_key: Some(state_key), + ..Default::default() + }, + sender_user, + space_id, + &state_lock, + ) + .boxed() + .await + .ok(); + // Now, add a new child event for the replacement room + services + .rooms + .timeline + .build_and_append_pdu( + PduBuilder { + event_type: StateEventType::SpaceChild.into(), + content: to_raw_value(&child).expect("event is valid, we just created it"), + state_key: Some(StateKey::new()), + ..Default::default() + }, + sender_user, + space_id, + &state_lock, + ) + .boxed() + .await + .ok(); + drop(state_lock); + } + // Return the replacement room id Ok(upgrade_room::v3::Response { replacement_room }) } diff --git a/src/service/rooms/state_accessor/room_state.rs b/src/service/rooms/state_accessor/room_state.rs index 89a66f0c..b5306485 100644 --- a/src/service/rooms/state_accessor/room_state.rs +++ b/src/service/rooms/state_accessor/room_state.rs @@ -91,3 +91,22 @@ pub async fn room_state_get( .and_then(|shortstatehash| self.state_get(shortstatehash, event_type, state_key)) .await } + +/// Returns all state keys for the given `room_id` and `event_type`. +#[implement(super::Service)] +#[tracing::instrument(skip(self), level = "debug")] +pub async fn room_state_keys( + &self, + room_id: &RoomId, + event_type: &StateEventType, +) -> Result> { + let shortstatehash = self.services.state.get_room_shortstatehash(room_id).await?; + + let state_keys: Vec = self + .state_keys(shortstatehash, event_type) + .map(|state_key| state_key.to_string()) + .collect() + .await; + + Ok(state_keys) +} diff --git a/src/service/rooms/state_accessor/user_can.rs b/src/service/rooms/state_accessor/user_can.rs index 221263a8..5bbed173 100644 --- a/src/service/rooms/state_accessor/user_can.rs +++ b/src/service/rooms/state_accessor/user_can.rs @@ -1,6 +1,6 @@ use conduwuit::{Err, Result, implement, matrix::Event, pdu::PduBuilder}; use ruma::{ - EventId, RoomId, UserId, + EventId, Int, RoomId, UserId, events::{ StateEventType, TimelineEventType, room::{ @@ -167,3 +167,33 @@ pub async fn user_can_invite( .await .is_ok() } + +#[implement(super::Service)] +pub async fn current_power_levels( + &self, + room_id: &RoomId, +) -> Result { + // fetches the current power levels event content for a room, returning the + // default power levels if no power levels event is found + let pl_event_content = self + .room_state_get_content::( + room_id, + &StateEventType::RoomPowerLevels, + "", + ) + .await; + if let Ok(pl_event_content) = pl_event_content { + Ok(pl_event_content) + } else { + let mut default_power_levels = RoomPowerLevelsEventContent::default(); + + // set the creator as PL100 + let create_event = self + .room_state_get(room_id, &StateEventType::RoomCreate, "") + .await?; + default_power_levels + .users + .insert(create_event.sender().to_owned(), Int::from(100)); + Ok(default_power_levels) + } +} From 3b5335630dacd8e85bfb2eba4e127f84eda1ce56 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 19 Jul 2025 15:17:27 +0100 Subject: [PATCH 3/8] feat(space-upgrades): Transfer all state keys during upgrade Before this change, only state events with an empty state key would be cloned. This allows m.space.child to be cloned appropriately. --- src/api/client/room/upgrade.rs | 53 +++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/api/client/room/upgrade.rs b/src/api/client/room/upgrade.rs index 3a0ed010..0d6d2805 100644 --- a/src/api/client/room/upgrade.rs +++ b/src/api/client/room/upgrade.rs @@ -212,32 +212,39 @@ pub(crate) async fn upgrade_room_route( // Replicate transferable state events to the new room for event_type in TRANSFERABLE_STATE_EVENTS { - let event_content = match services + let state_keys = services .rooms .state_accessor - .room_state_get(&body.room_id, event_type, "") - .await - { - | Ok(v) => v.content().to_owned(), - | Err(_) => continue, // Skipping missing events. - }; - - services - .rooms - .timeline - .build_and_append_pdu( - PduBuilder { - event_type: event_type.to_string().into(), - content: event_content, - state_key: Some(StateKey::new()), - ..Default::default() - }, - sender_user, - &replacement_room, - &state_lock, - ) - .boxed() + .room_state_keys(&body.room_id, event_type) .await?; + for state_key in state_keys { + let event_content = match services + .rooms + .state_accessor + .room_state_get(&body.room_id, event_type, &state_key) + .await + { + | Ok(v) => v.content().to_owned(), + | Err(_) => continue, // Skipping missing events. + }; + + services + .rooms + .timeline + .build_and_append_pdu( + PduBuilder { + event_type: event_type.to_string().into(), + content: event_content, + state_key: Some(StateKey::from(state_key)), + ..Default::default() + }, + sender_user, + &replacement_room, + &state_lock, + ) + .boxed() + .await?; + } } // Moves any local aliases to the new room From f063814d9457a5600536d78938b42860a89c6c0e Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 19 Jul 2025 15:22:50 +0100 Subject: [PATCH 4/8] fix(space-upgrades): Incorrectly updated parent children events --- src/api/client/room/upgrade.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/api/client/room/upgrade.rs b/src/api/client/room/upgrade.rs index 0d6d2805..0109715b 100644 --- a/src/api/client/room/upgrade.rs +++ b/src/api/client/room/upgrade.rs @@ -2,7 +2,7 @@ use std::cmp::max; use axum::extract::State; use conduwuit::{ - Err, Error, Event, Result, err, info, + Err, Error, Event, Result, debug, err, info, matrix::{StateKey, pdu::PduBuilder}, }; use futures::{FutureExt, StreamExt}; @@ -316,7 +316,6 @@ pub(crate) async fn upgrade_room_route( for raw_space_id in parents { let space_id = RoomId::parse(&raw_space_id)?; - let state_key = StateKey::from(raw_space_id.clone()); let Ok(child) = services .rooms .state_accessor @@ -330,8 +329,13 @@ pub(crate) async fn upgrade_room_route( // If the space does not have a child event for this room, we can skip it continue; }; + debug!( + "Updating space {space_id} child event for room {} to {replacement_room}", + &body.room_id + ); // First, drop the space's child event let state_lock = services.rooms.state.mutex.lock(space_id).await; + debug!("Removing space child event for room {} in space {space_id}", &body.room_id); services .rooms .timeline @@ -340,7 +344,7 @@ pub(crate) async fn upgrade_room_route( event_type: StateEventType::SpaceChild.into(), content: to_raw_value(&RedactedSpaceChildEventContent {}) .expect("event is valid, we just created it"), - state_key: Some(state_key), + state_key: Some(body.room_id.clone().as_str().into()), ..Default::default() }, sender_user, @@ -351,6 +355,7 @@ pub(crate) async fn upgrade_room_route( .await .ok(); // Now, add a new child event for the replacement room + debug!("Adding space child event for room {replacement_room} in space {space_id}"); services .rooms .timeline @@ -358,7 +363,7 @@ pub(crate) async fn upgrade_room_route( PduBuilder { event_type: StateEventType::SpaceChild.into(), content: to_raw_value(&child).expect("event is valid, we just created it"), - state_key: Some(StateKey::new()), + state_key: Some(replacement_room.as_str().into()), ..Default::default() }, sender_user, @@ -368,6 +373,10 @@ pub(crate) async fn upgrade_room_route( .boxed() .await .ok(); + debug!( + "Finished updating space {space_id} child event for room {} to {replacement_room}", + &body.room_id + ); drop(state_lock); } From 57868a008c9e1dc5ad9c86865b0e61421ac5670c Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 19 Jul 2025 15:41:36 +0100 Subject: [PATCH 5/8] feat(space-upgrades): Skip empty state events in room upgrade --- src/api/client/room/upgrade.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/api/client/room/upgrade.rs b/src/api/client/room/upgrade.rs index 0109715b..02495902 100644 --- a/src/api/client/room/upgrade.rs +++ b/src/api/client/room/upgrade.rs @@ -227,6 +227,10 @@ pub(crate) async fn upgrade_room_route( | Ok(v) => v.content().to_owned(), | Err(_) => continue, // Skipping missing events. }; + if event_content.get() == "{}" { + // If the event content is empty, we skip it + continue; + } services .rooms From b2b18002ea28858ec9f4193c2302ec5fbce19638 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 19 Jul 2025 15:44:49 +0100 Subject: [PATCH 6/8] fix(space-upgrades): Remove unused helper function --- src/service/rooms/state_accessor/user_can.rs | 30 -------------------- 1 file changed, 30 deletions(-) diff --git a/src/service/rooms/state_accessor/user_can.rs b/src/service/rooms/state_accessor/user_can.rs index 5bbed173..f7f3377c 100644 --- a/src/service/rooms/state_accessor/user_can.rs +++ b/src/service/rooms/state_accessor/user_can.rs @@ -167,33 +167,3 @@ pub async fn user_can_invite( .await .is_ok() } - -#[implement(super::Service)] -pub async fn current_power_levels( - &self, - room_id: &RoomId, -) -> Result { - // fetches the current power levels event content for a room, returning the - // default power levels if no power levels event is found - let pl_event_content = self - .room_state_get_content::( - room_id, - &StateEventType::RoomPowerLevels, - "", - ) - .await; - if let Ok(pl_event_content) = pl_event_content { - Ok(pl_event_content) - } else { - let mut default_power_levels = RoomPowerLevelsEventContent::default(); - - // set the creator as PL100 - let create_event = self - .room_state_get(room_id, &StateEventType::RoomCreate, "") - .await?; - default_power_levels - .users - .insert(create_event.sender().to_owned(), Int::from(100)); - Ok(default_power_levels) - } -} From 331832616f346050201d9949c9ee4493b8b1ef65 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 19 Jul 2025 15:51:03 +0100 Subject: [PATCH 7/8] feat(space-upgrades): MSC4168: Override space child vias --- src/api/client/room/upgrade.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/api/client/room/upgrade.rs b/src/api/client/room/upgrade.rs index 02495902..c2c3aa81 100644 --- a/src/api/client/room/upgrade.rs +++ b/src/api/client/room/upgrade.rs @@ -366,7 +366,12 @@ pub(crate) async fn upgrade_room_route( .build_and_append_pdu( PduBuilder { event_type: StateEventType::SpaceChild.into(), - content: to_raw_value(&child).expect("event is valid, we just created it"), + content: to_raw_value(&SpaceChildEventContent { + via: vec![sender_user.server_name().to_owned()], + order: child.order, + suggested: child.suggested, + }) + .expect("event is valid, we just created it"), state_key: Some(replacement_room.as_str().into()), ..Default::default() }, From c639228f4ddc14f73fb3bbcc1316ef9dbe024408 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 19 Jul 2025 15:57:53 +0100 Subject: [PATCH 8/8] style(space-upgrades): Remove unused import left over from 6691b7672b --- src/service/rooms/state_accessor/user_can.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/rooms/state_accessor/user_can.rs b/src/service/rooms/state_accessor/user_can.rs index f7f3377c..221263a8 100644 --- a/src/service/rooms/state_accessor/user_can.rs +++ b/src/service/rooms/state_accessor/user_can.rs @@ -1,6 +1,6 @@ use conduwuit::{Err, Result, implement, matrix::Event, pdu::PduBuilder}; use ruma::{ - EventId, Int, RoomId, UserId, + EventId, RoomId, UserId, events::{ StateEventType, TimelineEventType, room::{