diff --git a/src/api/client/room/upgrade.rs b/src/api/client/room/upgrade.rs index afd6f70e..c2c3aa81 100644 --- a/src/api/client/room/upgrade.rs +++ b/src/api/client/room/upgrade.rs @@ -2,10 +2,10 @@ 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::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,35 +207,48 @@ pub(crate) async fn upgrade_room_route( &replacement_room, &state_lock, ) + .boxed() .await?; // 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, - ) + .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. + }; + if event_content.get() == "{}" { + // If the event content is empty, we skip it + continue; + } + + 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 @@ -290,10 +305,90 @@ 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 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; + }; + 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 + .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(body.room_id.clone().as_str().into()), + ..Default::default() + }, + sender_user, + space_id, + &state_lock, + ) + .boxed() + .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 + .build_and_append_pdu( + PduBuilder { + event_type: StateEventType::SpaceChild.into(), + 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() + }, + sender_user, + space_id, + &state_lock, + ) + .boxed() + .await + .ok(); + debug!( + "Finished updating space {space_id} child event for room {} to {replacement_room}", + &body.room_id + ); + 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) +}