From b2883c3d6e43f736cc70959443cacd44c6a1b9c0 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 19 Jul 2025 15:08:21 +0100 Subject: [PATCH] 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) + } +}