Implement MSC4289/MSC4291 (room version 12) upgrade support. (closes #141)
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
@@ -3,7 +3,7 @@ use std::cmp::max;
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt};
|
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId, UserId,
|
CanonicalJsonObject, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId, UserId,
|
||||||
api::client::room::upgrade_room::v3,
|
api::client::room::upgrade_room::v3,
|
||||||
events::{
|
events::{
|
||||||
StateEventType, TimelineEventType,
|
StateEventType, TimelineEventType,
|
||||||
@@ -15,11 +15,14 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
int,
|
int,
|
||||||
room_version_rules::RoomIdFormatVersion,
|
room_version_rules::{RoomIdFormatVersion, RoomVersionRules},
|
||||||
|
};
|
||||||
|
use serde_json::{
|
||||||
|
Value as JsonValue, json,
|
||||||
|
value::{to_raw_value, to_value},
|
||||||
};
|
};
|
||||||
use serde_json::{json, value::to_raw_value};
|
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Result, err, error, implement, info,
|
Err, Result, debug_info, err, error, implement, info, is_equal_to, is_less_than,
|
||||||
matrix::{Event, StateKey, pdu::PduBuilder, room_version},
|
matrix::{Event, StateKey, pdu::PduBuilder, room_version},
|
||||||
utils::{
|
utils::{
|
||||||
future::TryExtExt,
|
future::TryExtExt,
|
||||||
@@ -43,14 +46,16 @@ const RECOMMENDED_TRANSFERABLE_STATE_EVENT_TYPES: &[StateEventType; 9] = &[
|
|||||||
StateEventType::RoomPowerLevels,
|
StateEventType::RoomPowerLevels,
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Debug)]
|
||||||
struct RoomUpgradeContext<'a> {
|
struct RoomUpgradeContext<'a> {
|
||||||
services: &'a Services,
|
services: &'a Services,
|
||||||
sender_user: &'a UserId,
|
sender_user: &'a UserId,
|
||||||
new_room_id: &'a RoomId,
|
|
||||||
new_state_lock: &'a RoomMutexGuard,
|
|
||||||
old_room_id: &'a RoomId,
|
old_room_id: &'a RoomId,
|
||||||
old_state_lock: &'a RoomMutexGuard,
|
old_state_lock: &'a RoomMutexGuard,
|
||||||
|
new_room_id: &'a RoomId,
|
||||||
|
new_state_lock: &'a RoomMutexGuard,
|
||||||
|
new_version_rules: &'a RoomVersionRules,
|
||||||
|
additional_creators: &'a [OwnedUserId],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
|
/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
|
||||||
@@ -81,12 +86,6 @@ pub(crate) async fn upgrade_room_route(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(new_version, RoomVersionId::V12) {
|
|
||||||
return Err!(Request(UnsupportedRoomVersion(
|
|
||||||
"Upgrading to version 12 is still under development.",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let old_room_id = &body.room_id;
|
let old_room_id = &body.room_id;
|
||||||
let old_state_lock = services.state.mutex.lock(old_room_id).await;
|
let old_state_lock = services.state.mutex.lock(old_room_id).await;
|
||||||
|
|
||||||
@@ -112,21 +111,50 @@ pub(crate) async fn upgrade_room_route(
|
|||||||
.map(ToOwned::to_owned),
|
.map(ToOwned::to_owned),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
debug_info!(
|
||||||
|
%sender_user,
|
||||||
|
%old_room_id,
|
||||||
|
last_event = ?predecessor.event_id,
|
||||||
|
?new_version,
|
||||||
|
"Attempting upgrade of room..."
|
||||||
|
);
|
||||||
|
|
||||||
let id_format = version_rules.room_id_format;
|
let id_format = version_rules.room_id_format;
|
||||||
let (replacement_room, state_lock) = match id_format {
|
let (replacement_room, state_lock) = match id_format {
|
||||||
| RoomIdFormatVersion::V1 => upgrade_room_create_legacy(&services, &body, predecessor),
|
| RoomIdFormatVersion::V2 =>
|
||||||
| _ => unimplemented!("Unexpected format {id_format:?} for room {new_version}"),
|
upgrade_room_create(
|
||||||
|
&services,
|
||||||
|
sender_user,
|
||||||
|
old_room_id,
|
||||||
|
new_version,
|
||||||
|
&version_rules,
|
||||||
|
predecessor,
|
||||||
|
body.additional_creators.clone(),
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
|
||||||
|
| RoomIdFormatVersion::V1 =>
|
||||||
|
upgrade_room_create_legacy(
|
||||||
|
&services,
|
||||||
|
sender_user,
|
||||||
|
old_room_id,
|
||||||
|
new_version,
|
||||||
|
&version_rules,
|
||||||
|
predecessor,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
}
|
}
|
||||||
.inspect_err(|e| error!(?body, "Upgrade creation event failed: {e}"))
|
.inspect_err(|e| error!(?body, "Upgrade m.room.create event failed: {e}"))?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let context = RoomUpgradeContext {
|
let context = RoomUpgradeContext {
|
||||||
services: &services,
|
services: &services,
|
||||||
sender_user,
|
sender_user,
|
||||||
new_room_id: &replacement_room,
|
|
||||||
new_state_lock: &state_lock,
|
|
||||||
old_room_id: &body.room_id,
|
old_room_id: &body.room_id,
|
||||||
old_state_lock: &old_state_lock,
|
old_state_lock: &old_state_lock,
|
||||||
|
new_room_id: &replacement_room,
|
||||||
|
new_state_lock: &state_lock,
|
||||||
|
new_version_rules: &version_rules,
|
||||||
|
additional_creators: &body.additional_creators,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = context.transfer_room().await {
|
if let Err(e) = context.transfer_room().await {
|
||||||
@@ -146,21 +174,88 @@ pub(crate) async fn upgrade_room_route(
|
|||||||
info!(
|
info!(
|
||||||
old_room_id = %context.old_room_id,
|
old_room_id = %context.old_room_id,
|
||||||
new_room_id = %context.new_room_id,
|
new_room_id = %context.new_room_id,
|
||||||
|
upgraded_by = %sender_user,
|
||||||
"Room upgraded",
|
"Room upgraded",
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(v3::Response { replacement_room })
|
Ok(v3::Response { replacement_room })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "info")]
|
||||||
|
async fn upgrade_room_create(
|
||||||
|
services: &Services,
|
||||||
|
sender_user: &UserId,
|
||||||
|
old_room_id: &RoomId,
|
||||||
|
new_version: &RoomVersionId,
|
||||||
|
version_rules: &RoomVersionRules,
|
||||||
|
predecessor: PreviousRoom,
|
||||||
|
mut additional_creators: Vec<OwnedUserId>,
|
||||||
|
) -> Result<(OwnedRoomId, RoomMutexGuard)> {
|
||||||
|
// Get the old room creation event
|
||||||
|
let mut content: CanonicalJsonObject = services
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get_content(old_room_id, &StateEventType::RoomCreate, "")
|
||||||
|
.await
|
||||||
|
.map_err(|_| err!(Database("Found room without m.room.create event.")))?;
|
||||||
|
|
||||||
|
content.remove("creator");
|
||||||
|
content.insert("predecessor".into(), json!(predecessor).try_into()?);
|
||||||
|
content.insert("room_version".into(), json!(new_version).try_into()?);
|
||||||
|
|
||||||
|
if version_rules
|
||||||
|
.authorization
|
||||||
|
.additional_room_creators
|
||||||
|
{
|
||||||
|
additional_creators.sort();
|
||||||
|
additional_creators.dedup();
|
||||||
|
content.remove("additional_creators");
|
||||||
|
if !additional_creators.is_empty() {
|
||||||
|
content.insert("additional_creators".into(), json!(additional_creators).try_into()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate creation event content
|
||||||
|
let raw_content = to_raw_value(&content)?;
|
||||||
|
if let Err(e) = serde_json::from_str::<CanonicalJsonObject>(raw_content.get()) {
|
||||||
|
return Err!(Request(BadJson("Error forming creation event: {e}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let room_id = ruma::room_id!("!thiswillbereplaced").to_owned();
|
||||||
|
let state_lock = services.state.mutex.lock(&room_id).await;
|
||||||
|
let create_event_id = services
|
||||||
|
.timeline
|
||||||
|
.build_and_append_pdu(
|
||||||
|
PduBuilder {
|
||||||
|
event_type: TimelineEventType::RoomCreate,
|
||||||
|
content: to_raw_value(&content)?,
|
||||||
|
state_key: Some(StateKey::new()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
sender_user,
|
||||||
|
&room_id,
|
||||||
|
&state_lock,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
drop(state_lock);
|
||||||
|
|
||||||
|
// The real room_id is now the event_id.
|
||||||
|
let room_id = OwnedRoomId::from_parts('!', create_event_id.localpart(), None)?;
|
||||||
|
let state_lock = services.state.mutex.lock(&room_id).await;
|
||||||
|
|
||||||
|
Ok((room_id, state_lock))
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "info")]
|
#[tracing::instrument(level = "info")]
|
||||||
async fn upgrade_room_create_legacy(
|
async fn upgrade_room_create_legacy(
|
||||||
services: &Services,
|
services: &Services,
|
||||||
body: &Ruma<v3::Request>,
|
sender_user: &UserId,
|
||||||
|
old_room_id: &RoomId,
|
||||||
|
new_version: &RoomVersionId,
|
||||||
|
version_rules: &RoomVersionRules,
|
||||||
predecessor: PreviousRoom,
|
predecessor: PreviousRoom,
|
||||||
) -> Result<(OwnedRoomId, RoomMutexGuard)> {
|
) -> Result<(OwnedRoomId, RoomMutexGuard)> {
|
||||||
let sender_user = body.sender_user();
|
|
||||||
let old_room_id = &body.room_id;
|
|
||||||
|
|
||||||
// Create a replacement room
|
// Create a replacement room
|
||||||
let new_room_id = RoomId::new_v1(services.globals.server_name());
|
let new_room_id = RoomId::new_v1(services.globals.server_name());
|
||||||
let state_lock = services.state.mutex.lock(&new_room_id).await;
|
let state_lock = services.state.mutex.lock(&new_room_id).await;
|
||||||
@@ -170,7 +265,7 @@ async fn upgrade_room_create_legacy(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Get the old room creation event
|
// Get the old room creation event
|
||||||
let mut create_event_content: CanonicalJsonObject = services
|
let mut content: CanonicalJsonObject = services
|
||||||
.state_accessor
|
.state_accessor
|
||||||
.room_state_get_content(old_room_id, &StateEventType::RoomCreate, "")
|
.room_state_get_content(old_room_id, &StateEventType::RoomCreate, "")
|
||||||
.await
|
.await
|
||||||
@@ -180,18 +275,18 @@ async fn upgrade_room_create_legacy(
|
|||||||
// room_version. "creator" key no longer exists in V11+ rooms.
|
// room_version. "creator" key no longer exists in V11+ rooms.
|
||||||
{
|
{
|
||||||
use RoomVersionId::*;
|
use RoomVersionId::*;
|
||||||
match body.new_version {
|
match new_version {
|
||||||
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
|
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
|
||||||
create_event_content.insert("creator".into(), json!(&sender_user).try_into()?),
|
content.insert("creator".into(), json!(&sender_user).try_into()?),
|
||||||
| _ => create_event_content.remove("creator"),
|
| _ => content.remove("creator"),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
create_event_content.insert("predecessor".into(), json!(predecessor).try_into()?);
|
content.insert("predecessor".into(), json!(predecessor).try_into()?);
|
||||||
create_event_content.insert("room_version".into(), json!(&body.new_version).try_into()?);
|
content.insert("room_version".into(), json!(new_version).try_into()?);
|
||||||
|
|
||||||
// Validate creation event content
|
// Validate creation event content
|
||||||
let raw_content = to_raw_value(&create_event_content)?;
|
let raw_content = to_raw_value(&content)?;
|
||||||
if let Err(e) = serde_json::from_str::<CanonicalJsonObject>(raw_content.get()) {
|
if let Err(e) = serde_json::from_str::<CanonicalJsonObject>(raw_content.get()) {
|
||||||
return Err!(Request(BadJson("Error forming creation event: {e}")));
|
return Err!(Request(BadJson("Error forming creation event: {e}")));
|
||||||
}
|
}
|
||||||
@@ -201,7 +296,7 @@ async fn upgrade_room_create_legacy(
|
|||||||
.build_and_append_pdu(
|
.build_and_append_pdu(
|
||||||
PduBuilder {
|
PduBuilder {
|
||||||
event_type: TimelineEventType::RoomCreate,
|
event_type: TimelineEventType::RoomCreate,
|
||||||
content: to_raw_value(&create_event_content)?,
|
content: to_raw_value(&content)?,
|
||||||
state_key: Some(StateKey::new()),
|
state_key: Some(StateKey::new()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -279,17 +374,10 @@ async fn move_state_events(&self) -> Result {
|
|||||||
})
|
})
|
||||||
.map(Ok)
|
.map(Ok)
|
||||||
.try_for_each(async |event| {
|
.try_for_each(async |event| {
|
||||||
let builder = PduBuilder {
|
|
||||||
event_type: event.kind().clone(),
|
|
||||||
content: to_raw_value(event.content())?,
|
|
||||||
state_key: Some(StateKey::new()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
self.services
|
self.services
|
||||||
.timeline
|
.timeline
|
||||||
.build_and_append_pdu(
|
.build_and_append_pdu(
|
||||||
builder,
|
self.rebuild_state_event(&event)?,
|
||||||
self.sender_user,
|
self.sender_user,
|
||||||
self.new_room_id,
|
self.new_room_id,
|
||||||
self.new_state_lock,
|
self.new_state_lock,
|
||||||
@@ -303,6 +391,53 @@ async fn move_state_events(&self) -> Result {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[implement(RoomUpgradeContext, params = "<'_>")]
|
||||||
|
#[tracing::instrument(level = "debug")]
|
||||||
|
fn rebuild_state_event<Pdu: Event>(&self, event: &Pdu) -> Result<PduBuilder> {
|
||||||
|
let content = match event.kind() {
|
||||||
|
| TimelineEventType::RoomPowerLevels
|
||||||
|
if self
|
||||||
|
.new_version_rules
|
||||||
|
.authorization
|
||||||
|
.explicitly_privilege_room_creators =>
|
||||||
|
{
|
||||||
|
let mut content = event.get_content_as_value();
|
||||||
|
|
||||||
|
if let Some(users) = content
|
||||||
|
.get_mut("users")
|
||||||
|
.and_then(JsonValue::as_object_mut)
|
||||||
|
{
|
||||||
|
users.retain(|user_id, _pl| {
|
||||||
|
!self
|
||||||
|
.additional_creators
|
||||||
|
.iter()
|
||||||
|
.map(AsRef::as_ref)
|
||||||
|
.map(UserId::as_str)
|
||||||
|
.any(is_equal_to!(user_id.as_str()))
|
||||||
|
&& self.sender_user.as_str() != user_id.as_str()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if content["events"]["m.room.tombstone"]
|
||||||
|
.as_i64()
|
||||||
|
.is_none_or(is_less_than!(150))
|
||||||
|
{
|
||||||
|
content["events"]["m.room.tombstone"] = to_value(150)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
to_raw_value(&content)?
|
||||||
|
},
|
||||||
|
| _ => to_raw_value(event.content())?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PduBuilder {
|
||||||
|
content,
|
||||||
|
event_type: event.kind().clone(),
|
||||||
|
state_key: event.state_key().map(Into::into),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Moves any local aliases to the new room
|
// Moves any local aliases to the new room
|
||||||
#[implement(RoomUpgradeContext, params = "<'_>")]
|
#[implement(RoomUpgradeContext, params = "<'_>")]
|
||||||
#[tracing::instrument(level = "debug")]
|
#[tracing::instrument(level = "debug")]
|
||||||
@@ -339,7 +474,7 @@ async fn tombstone_old_room(&self) -> Result<OwnedEventId> {
|
|||||||
.timeline
|
.timeline
|
||||||
.build_and_append_pdu(
|
.build_and_append_pdu(
|
||||||
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||||
body: "This room has been upgraded".to_owned(),
|
body: "This room has been upgraded.".to_owned(),
|
||||||
replacement_room: self.new_room_id.to_owned(),
|
replacement_room: self.new_room_id.to_owned(),
|
||||||
}),
|
}),
|
||||||
self.sender_user,
|
self.sender_user,
|
||||||
|
|||||||
@@ -324,7 +324,7 @@
|
|||||||
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators_AdditionalValidation/additional_creators_elements_aren't_valid_user_ID_strings_(domain)"}
|
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators_AdditionalValidation/additional_creators_elements_aren't_valid_user_ID_strings_(domain)"}
|
||||||
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators_AdditionalValidation/additional_creators_isn't_an_array"}
|
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators_AdditionalValidation/additional_creators_isn't_an_array"}
|
||||||
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators_InvitedAreCreators"}
|
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators_InvitedAreCreators"}
|
||||||
{"Action":"fail","Test":"TestMSC4289PrivilegedRoomCreators_Upgrades"}
|
{"Action":"pass","Test":"TestMSC4289PrivilegedRoomCreators_Upgrades"}
|
||||||
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent"}
|
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent"}
|
||||||
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent"}
|
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent"}
|
||||||
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent_CannotSendCreateEvent"}
|
{"Action":"pass","Test":"TestMSC4291RoomIDAsHashOfCreateEvent_CannotSendCreateEvent"}
|
||||||
|
|||||||
Reference in New Issue
Block a user