Compare commits

..

8 commits

Author SHA1 Message Date
nexy7574
c639228f4d
style(space-upgrades): Remove unused import left over from 6691b7672b
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 4s
Release Docker Image / define-variables (push) Failing after 2s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 1s
Checks / Rust / Clippy (push) Failing after 22s
Checks / Rust / Cargo Test (push) Failing after 21s
2025-07-19 18:37:45 +01:00
nexy7574
331832616f
feat(space-upgrades): MSC4168: Override space child vias 2025-07-19 18:37:45 +01:00
nexy7574
b2b18002ea
fix(space-upgrades): Remove unused helper function 2025-07-19 18:37:45 +01:00
nexy7574
57868a008c
feat(space-upgrades): Skip empty state events in room upgrade 2025-07-19 18:37:45 +01:00
nexy7574
f063814d94
fix(space-upgrades): Incorrectly updated parent children events 2025-07-19 18:37:38 +01:00
nexy7574
3b5335630d
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.
2025-07-19 18:35:59 +01:00
nexy7574
b2883c3d6e
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.
2025-07-19 18:35:58 +01:00
nexy7574
62bdfe1ce8
feat(space-upgrades): Copy over space child & parent states 2025-07-19 18:35:56 +01:00
2 changed files with 145 additions and 31 deletions

View file

@ -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,15 +16,16 @@ 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; 9] = &[
const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 11] = &[
StateEventType::RoomAvatar,
StateEventType::RoomEncryption,
StateEventType::RoomGuestAccess,
@ -34,6 +35,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 +54,7 @@ pub(crate) async fn upgrade_room_route(
State(services): State<crate::State>,
body: Ruma<upgrade_room::v3::Request>,
) -> Result<upgrade_room::v3::Response> {
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) {
@ -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,19 +207,30 @@ 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 state_keys = services
.rooms
.state_accessor
.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, "")
.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
@ -226,15 +239,17 @@ pub(crate) async fn upgrade_room_route(
PduBuilder {
event_type: event_type.to_string().into(),
content: event_content,
state_key: Some(StateKey::new()),
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
let mut local_aliases = services
@ -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::<SpaceChildEventContent>(
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 })
}

View file

@ -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<Vec<String>> {
let shortstatehash = self.services.state.get_room_shortstatehash(room_id).await?;
let state_keys: Vec<String> = self
.state_keys(shortstatehash, event_type)
.map(|state_key| state_key.to_string())
.collect()
.await;
Ok(state_keys)
}