mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
655 lines
18 KiB
Rust
655 lines
18 KiB
Rust
use std::cmp::max;
|
|
|
|
use axum::extract::State;
|
|
use conduwuit::{
|
|
Err, Error, Event, Result, debug,
|
|
debug::DebugInspect,
|
|
err, error,
|
|
info::room_version::UNSTABLE_ROOM_VERSIONS,
|
|
matrix::{StateKey, pdu::PartialPdu},
|
|
};
|
|
use futures::{FutureExt, StreamExt};
|
|
use ruma::{
|
|
OwnedEventId, OwnedRoomId, RoomId, UserId,
|
|
api::{client::room::upgrade_room, error::ErrorKind},
|
|
assign,
|
|
events::{
|
|
StateEventType,
|
|
room::{
|
|
create::{PreviousRoom, RoomCreateEventContent},
|
|
member::{MembershipState, RoomMemberEventContent},
|
|
power_levels::RoomPowerLevelsEventContent,
|
|
tombstone::RoomTombstoneEventContent,
|
|
},
|
|
space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
|
|
},
|
|
int,
|
|
room_version_rules::RoomIdFormatVersion,
|
|
};
|
|
use serde_json::value::to_raw_value;
|
|
|
|
use crate::router::Ruma;
|
|
|
|
/// Recommended transferable state events list from the spec
|
|
const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 11] = &[
|
|
StateEventType::RoomServerAcl,
|
|
StateEventType::RoomEncryption,
|
|
StateEventType::RoomName,
|
|
StateEventType::RoomAvatar,
|
|
StateEventType::RoomTopic,
|
|
StateEventType::RoomGuestAccess,
|
|
StateEventType::RoomHistoryVisibility,
|
|
StateEventType::RoomJoinRules,
|
|
StateEventType::RoomPowerLevels,
|
|
StateEventType::SpaceParent,
|
|
StateEventType::SpaceChild,
|
|
];
|
|
|
|
/// Updates spaces that are marked as parents of old_room_id to instead point to
|
|
/// the new room ID.
|
|
///
|
|
/// See: https://github.com/matrix-org/matrix-spec-proposals/pull/4168
|
|
async fn update_parents(
|
|
services: &crate::State,
|
|
sender: &UserId,
|
|
old_room_id: &RoomId,
|
|
new_room_id: &RoomId,
|
|
) -> Result {
|
|
// Fetch the spaces which this room claims are its parents.
|
|
|
|
// In rooms that reference the old room via m.space.child events...
|
|
let parents = services
|
|
.rooms
|
|
.state_accessor
|
|
.room_state_keys(old_room_id, &StateEventType::SpaceParent)
|
|
.await
|
|
.debug_inspect(|k| debug!(?old_room_id, "Parents: {k:?}"))?;
|
|
|
|
for raw_parent_id in parents {
|
|
let parent_id = RoomId::parse(&raw_parent_id)?;
|
|
if !services
|
|
.rooms
|
|
.state_cache
|
|
.is_joined(sender, &parent_id)
|
|
.await
|
|
{
|
|
debug!(%parent_id, "Skipping space as sender is not joined");
|
|
continue; // Skip updating rooms the sender isn't in.
|
|
}
|
|
let state_lock = services.rooms.state.mutex.lock(parent_id.as_str()).await;
|
|
// We're now fetching state from the *space* that has the old room as a *child*.
|
|
// Follow along. This will be on the test.
|
|
let Ok(child) = services
|
|
.rooms
|
|
.state_accessor
|
|
.room_state_get_content::<SpaceChildEventContent>(
|
|
&parent_id,
|
|
&StateEventType::SpaceChild,
|
|
old_room_id.as_str(),
|
|
)
|
|
.await
|
|
.debug_inspect_err(|e| {
|
|
error!(
|
|
?parent_id,
|
|
old_room_id=?old_room_id,
|
|
new_room_id=?new_room_id,
|
|
%e,
|
|
"failed to fetch m.space.child from parent"
|
|
);
|
|
})
|
|
else {
|
|
// If the space does not have a child event for this room, we can skip it
|
|
continue;
|
|
};
|
|
|
|
// ...the upgrading server SHOULD send a new m.space.child event with state_key
|
|
// set to the new room's ID, copying the order and suggested fields from the
|
|
// content of the m.space.child with state_key of the previous room ID.
|
|
services
|
|
.rooms
|
|
.timeline
|
|
.build_and_append_pdu(
|
|
PartialPdu::state(
|
|
new_room_id.as_str(),
|
|
&assign!(
|
|
SpaceChildEventContent::new(vec![sender.server_name().to_owned()]),
|
|
{
|
|
order: child.order,
|
|
suggested: child.suggested,
|
|
}
|
|
),
|
|
),
|
|
sender,
|
|
Some(&parent_id),
|
|
&state_lock,
|
|
)
|
|
.boxed()
|
|
.await
|
|
.debug_inspect_err(|e| {
|
|
error!(
|
|
?parent_id,
|
|
old_room_id=?old_room_id,
|
|
new_room_id=?new_room_id,
|
|
%e,
|
|
"failed to send m.space.child to parent during room upgrade"
|
|
);
|
|
})
|
|
.ok();
|
|
drop(state_lock);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// If the room being upgraded is a space, replace all m.space.parent references
|
|
/// in its children to point at the newly upgraded room ID, so that they point
|
|
/// at the new space.
|
|
///
|
|
/// See: https://github.com/matrix-org/matrix-spec-proposals/pull/4168
|
|
async fn update_children(
|
|
services: &crate::State,
|
|
sender: &UserId,
|
|
old_room_id: &RoomId,
|
|
new_room_id: &RoomId,
|
|
) -> Result {
|
|
// Fetch the children of this space.
|
|
// Note that this might not actually be a space, but just a room that has
|
|
// children.
|
|
|
|
// In rooms that reference the old room via m.space.parent events...
|
|
// NOTE: Doing that would be expensive. We'll instead fetch rooms which the
|
|
// space claims are children.
|
|
let parents = services
|
|
.rooms
|
|
.state_accessor
|
|
.room_state_keys(old_room_id, &StateEventType::SpaceChild)
|
|
.await
|
|
.debug_inspect(|k| debug!(?old_room_id, "Children: {k:?}"))?;
|
|
|
|
for raw_child_id in parents {
|
|
let child_id = RoomId::parse(&raw_child_id)?;
|
|
if !services
|
|
.rooms
|
|
.state_cache
|
|
.is_joined(sender, &child_id)
|
|
.await
|
|
{
|
|
debug!(%child_id, "Skipping child room as sender is not joined");
|
|
continue;
|
|
}
|
|
let state_lock = services.rooms.state.mutex.lock(child_id.as_str()).await;
|
|
// We're now fetching state from the *child* that has the old space as a
|
|
// *parent*. Follow along. This will also be on the test.
|
|
let Ok(ref parent) = services
|
|
.rooms
|
|
.state_accessor
|
|
.room_state_get_content::<SpaceParentEventContent>(
|
|
&child_id,
|
|
&StateEventType::SpaceParent,
|
|
old_room_id.as_str(),
|
|
)
|
|
.await
|
|
.debug_inspect_err(|e| {
|
|
error!(
|
|
?child_id,
|
|
old_room_id=?old_room_id,
|
|
new_room_id=?new_room_id,
|
|
%e,
|
|
"failed to fetch m.space.parent from child"
|
|
);
|
|
})
|
|
else {
|
|
// If the child does not have a parent event for this room, we can skip it.
|
|
continue;
|
|
};
|
|
|
|
// ... the upgrading server SHOULD send a new m.space.parent event with
|
|
// state_key set to the new room's ID.
|
|
services
|
|
.rooms
|
|
.timeline
|
|
.build_and_append_pdu(
|
|
PartialPdu::state(
|
|
new_room_id.as_str(),
|
|
&assign!(SpaceParentEventContent::new(vec![sender.server_name().to_owned()]), { canonical: parent.canonical }),
|
|
),
|
|
sender,
|
|
Some(&child_id),
|
|
&state_lock,
|
|
)
|
|
.boxed()
|
|
.await
|
|
.debug_inspect_err(|e| error!(
|
|
child_id=?child_id,
|
|
old_room_id=?old_room_id,
|
|
new_room_id=?new_room_id,
|
|
%e,
|
|
"failed to send updated m.space.parent to child during room upgrade"
|
|
))
|
|
.ok();
|
|
|
|
// If the previous m.space.parent event has canonical set to true in content,
|
|
// homeservers SHOULD update the old state event to set canonical to false,
|
|
// while setting it to true in the newly-sent m.space.parent event.
|
|
if parent.canonical {
|
|
services
|
|
.rooms
|
|
.timeline
|
|
.build_and_append_pdu(
|
|
PartialPdu::state(
|
|
old_room_id.as_str(),
|
|
&assign!(parent.clone(), {canonical: false}),
|
|
),
|
|
sender,
|
|
Some(&child_id),
|
|
&state_lock,
|
|
)
|
|
.boxed()
|
|
.await
|
|
.debug_inspect_err(|e| {
|
|
error!(
|
|
child_id=?child_id,
|
|
old_room_id=?old_room_id,
|
|
new_room_id=?new_room_id,
|
|
%e,
|
|
"failed to send non-canonical m.space.parent to child room"
|
|
);
|
|
})
|
|
.ok();
|
|
}
|
|
drop(state_lock);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
|
|
///
|
|
/// Upgrades the room.
|
|
///
|
|
/// - Creates a replacement room
|
|
/// - Sends a tombstone event into the current room
|
|
/// - Sender user joins the room
|
|
/// - Transfers some state events
|
|
/// - Moves local aliases
|
|
/// - Modifies old room power levels to prevent users from speaking
|
|
pub(crate) async fn upgrade_room_route(
|
|
State(services): State<crate::State>,
|
|
body: Ruma<upgrade_room::v3::Request>,
|
|
) -> Result<upgrade_room::v3::Response> {
|
|
let sender_user = body.identity.sender_user();
|
|
|
|
let (supported, forbid_unstable, is_unstable) = (
|
|
services.server.supported_room_version(&body.new_version),
|
|
!services.config.allow_unstable_room_versions,
|
|
UNSTABLE_ROOM_VERSIONS.contains(&body.new_version),
|
|
);
|
|
if !supported || (forbid_unstable && is_unstable) {
|
|
return Err(Error::BadRequest(
|
|
ErrorKind::UnsupportedRoomVersion,
|
|
"This server does not support that room version.",
|
|
));
|
|
}
|
|
|
|
if services.users.is_suspended(sender_user).await? {
|
|
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
|
}
|
|
|
|
// Make sure this isn't the admin room
|
|
// Admin room upgrades are hacky and should be done manually instead.
|
|
if services.admin.is_admin_room(&body.room_id).await {
|
|
return Err!(Request(Forbidden("Upgrading the admin room this way is not allowed.")));
|
|
}
|
|
|
|
// 1. Check that the user has permission to send m.room.tombstone events in the
|
|
// room.
|
|
let old_room_state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
|
|
|
|
// Check tombstone permission by attempting to create (but not send) the event.
|
|
services
|
|
.rooms
|
|
.timeline
|
|
.create_event(
|
|
PartialPdu::state(
|
|
StateKey::new(),
|
|
&RoomTombstoneEventContent::new(
|
|
String::new(),
|
|
RoomId::new_v1(services.globals.server_name()),
|
|
),
|
|
),
|
|
sender_user,
|
|
Some(&body.room_id),
|
|
&old_room_state_lock,
|
|
)
|
|
.await
|
|
.map_err(|_| {
|
|
err!(Request(Forbidden("You do not have permission to upgrade this room.")))
|
|
})?;
|
|
|
|
// Create a replacement room
|
|
let new_version_rules = body
|
|
.new_version
|
|
.rules()
|
|
.expect("new room version should have defined rules");
|
|
|
|
let last_event = if new_version_rules
|
|
.authorization
|
|
.room_create_event_id_as_room_id
|
|
{
|
|
None
|
|
} else {
|
|
Some(
|
|
services
|
|
.rooms
|
|
.state
|
|
.get_forward_extremities(&body.room_id)
|
|
.collect::<Vec<OwnedEventId>>()
|
|
.await[0]
|
|
.clone(),
|
|
)
|
|
};
|
|
let old_create_event: RoomCreateEventContent = services
|
|
.rooms
|
|
.state_accessor
|
|
.room_state_get_content(&body.room_id, &StateEventType::RoomCreate, "")
|
|
.await
|
|
.map_err(|_| err!(Database("Found room without m.room.create event.")))?;
|
|
let create_event_content = if new_version_rules.authorization.use_room_create_sender {
|
|
RoomCreateEventContent::new_v1(sender_user.to_owned())
|
|
} else {
|
|
RoomCreateEventContent::new_v11()
|
|
};
|
|
#[allow(deprecated)]
|
|
let create_event_content = {
|
|
assign!(
|
|
create_event_content,
|
|
{
|
|
additional_creators: if new_version_rules.authorization.additional_room_creators {
|
|
body.additional_creators.clone()
|
|
} else { Vec::new() },
|
|
creator: if new_version_rules.authorization.use_room_create_sender {
|
|
None
|
|
} else { Some(sender_user.to_owned()) },
|
|
predecessor: Some(assign!(PreviousRoom::new(body.room_id.clone()), {
|
|
event_id: last_event,
|
|
})),
|
|
room_type: old_create_event.room_type.clone(),
|
|
room_version: body.new_version.clone(),
|
|
}
|
|
)
|
|
};
|
|
|
|
let replacement_room_id: Option<OwnedRoomId> =
|
|
if new_version_rules.room_id_format == RoomIdFormatVersion::V2 {
|
|
None
|
|
} else {
|
|
Some(RoomId::new_v1(services.globals.server_name()))
|
|
};
|
|
|
|
let new_room_state_lock = if let Some(new_room_id) = replacement_room_id.as_ref() {
|
|
services.rooms.state.mutex.lock(new_room_id.as_str()).await
|
|
} else {
|
|
// NOTE: Using a hardcoded room ID for the temporary mutex means only one room
|
|
// can be created at a time. This is actually beneficial, as it reduces the
|
|
// risk of concurrent in-flight collisions.
|
|
services.rooms.state.mutex.lock("!new-room").await
|
|
};
|
|
debug!("Upgrading {} to room version {}", &body.room_id, &body.new_version);
|
|
let create_event_id = services
|
|
.rooms
|
|
.timeline
|
|
.build_and_append_pdu(
|
|
PartialPdu::state(StateKey::new(), &create_event_content),
|
|
sender_user,
|
|
replacement_room_id.as_deref(),
|
|
&new_room_state_lock,
|
|
)
|
|
.boxed()
|
|
.await?;
|
|
drop(new_room_state_lock);
|
|
// re-acquire a new lock with the new room ID.
|
|
// We don't actually need a state lock for sending the m.room.create event, but
|
|
// we get one anyway because the function requires it and I can't be bothered
|
|
// refactoring it.
|
|
let (replacement_room_id, new_room_state_lock) =
|
|
if new_version_rules.room_id_format == RoomIdFormatVersion::V2 {
|
|
let parsed_room_id = RoomId::new_v2(
|
|
create_event_id
|
|
.as_str()
|
|
.strip_prefix("$")
|
|
.expect("event ID must start with $ sigil"),
|
|
)?;
|
|
let lock = services
|
|
.rooms
|
|
.state
|
|
.mutex
|
|
.lock(parsed_room_id.as_str())
|
|
.await;
|
|
(Some(parsed_room_id), lock)
|
|
} else {
|
|
let new_room_id =
|
|
replacement_room_id.expect("replacement room id should be known by now");
|
|
let lock = services.rooms.state.mutex.lock(new_room_id.as_str()).await;
|
|
(Some(new_room_id), lock)
|
|
};
|
|
|
|
debug!("Upgraded {} to {}", &body.room_id, replacement_room_id.as_deref().unwrap());
|
|
// Join the new room
|
|
services
|
|
.rooms
|
|
.timeline
|
|
.build_and_append_pdu(
|
|
PartialPdu::state(
|
|
sender_user.as_str(),
|
|
&assign!(RoomMemberEventContent::new(MembershipState::Join), {
|
|
displayname: services.users.displayname(sender_user).await.ok(),
|
|
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
|
}),
|
|
),
|
|
sender_user,
|
|
replacement_room_id.as_deref(),
|
|
&new_room_state_lock,
|
|
)
|
|
.boxed()
|
|
.await?;
|
|
|
|
// 3. 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 mut 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 this is a power levels event, and the new room version has creators,
|
|
// we need to make sure they dont appear in the users block of power levels.
|
|
if *event_type == StateEventType::RoomPowerLevels {
|
|
let creators = body
|
|
.additional_creators
|
|
.clone()
|
|
.iter()
|
|
.chain(std::iter::once(&sender_user.to_owned()))
|
|
.map(ToOwned::to_owned)
|
|
.collect::<Vec<_>>();
|
|
let mut power_levels_event_content: RoomPowerLevelsEventContent =
|
|
serde_json::from_str(event_content.get()).map_err(|_| {
|
|
err!(Request(BadJson("Power levels event content is not valid")))
|
|
})?;
|
|
for creator in creators {
|
|
if new_version_rules
|
|
.authorization
|
|
.explicitly_privilege_room_creators
|
|
{
|
|
power_levels_event_content.users.remove(&creator);
|
|
} else {
|
|
power_levels_event_content.users.insert(
|
|
creator.clone(),
|
|
max(
|
|
int!(100),
|
|
power_levels_event_content
|
|
.users
|
|
.get(&creator)
|
|
.copied()
|
|
.unwrap_or_default(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
event_content = to_raw_value(&power_levels_event_content)
|
|
.expect("event is valid, we just deserialized and modified it");
|
|
}
|
|
|
|
debug!(%event_type, ?state_key, "Transferring state event to new room");
|
|
services
|
|
.rooms
|
|
.timeline
|
|
.build_and_append_pdu(
|
|
PartialPdu {
|
|
event_type: event_type.to_string().into(),
|
|
content: event_content,
|
|
state_key: Some(StateKey::from(state_key)),
|
|
..Default::default()
|
|
},
|
|
sender_user,
|
|
replacement_room_id.as_deref(),
|
|
&new_room_state_lock,
|
|
)
|
|
.boxed()
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
// 4. Move any local aliases to the new room
|
|
let mut local_aliases = services
|
|
.rooms
|
|
.alias
|
|
.local_aliases_for_room(&body.room_id)
|
|
.boxed();
|
|
|
|
while let Some(alias) = local_aliases.next().await {
|
|
debug!(?alias, "Migrating alias");
|
|
services
|
|
.rooms
|
|
.alias
|
|
.remove_alias(&alias, sender_user)
|
|
.await?;
|
|
|
|
services.rooms.alias.set_alias(
|
|
&alias,
|
|
replacement_room_id.as_deref().unwrap(),
|
|
sender_user,
|
|
)?;
|
|
}
|
|
|
|
// 5. Send a `m.room.tombstone` event to the old room to indicate that it is not
|
|
// intended to be used any further.
|
|
debug!(target=?body.room_id, "Sending tombstone to old room");
|
|
services
|
|
.rooms
|
|
.timeline
|
|
.build_and_append_pdu(
|
|
PartialPdu::state(
|
|
StateKey::new(),
|
|
&RoomTombstoneEventContent::new(
|
|
"This room has been replaced".to_owned(),
|
|
replacement_room_id.clone().unwrap(),
|
|
),
|
|
),
|
|
sender_user,
|
|
Some(&body.room_id),
|
|
&old_room_state_lock,
|
|
)
|
|
.await?;
|
|
|
|
// Get the old room power levels
|
|
let mut power_levels = services
|
|
.rooms
|
|
.state_accessor
|
|
.get_room_power_levels(&body.room_id)
|
|
.await;
|
|
|
|
// Setting events_default and invite to the greater of 50 and users_default + 1
|
|
let new_level = max(
|
|
int!(50),
|
|
power_levels
|
|
.users_default
|
|
.checked_add(int!(1))
|
|
.ok_or_else(|| {
|
|
err!(Request(BadJson("users_default power levels event content is not valid")))
|
|
})?,
|
|
);
|
|
|
|
power_levels.events_default = new_level;
|
|
power_levels.invite = new_level;
|
|
|
|
// 6. Modify the power levels in the old room to prevent sending of events and
|
|
// inviting new users
|
|
// Spec dictates that this is allowed to fail.
|
|
debug!(target=?body.room_id, ?new_level, "Raising power level in old room to lock it");
|
|
services
|
|
.rooms
|
|
.timeline
|
|
.build_and_append_pdu(
|
|
PartialPdu::state(
|
|
StateKey::new(),
|
|
&RoomPowerLevelsEventContent::try_from(power_levels).unwrap(),
|
|
),
|
|
sender_user,
|
|
Some(&body.room_id),
|
|
&old_room_state_lock,
|
|
)
|
|
.boxed()
|
|
.await
|
|
.ok();
|
|
|
|
// MSC4168: Update spaces that reference this room to point at the new room.
|
|
debug!("Updating parent spaces");
|
|
update_parents(
|
|
&services,
|
|
sender_user,
|
|
&body.room_id,
|
|
replacement_room_id.as_deref().unwrap(),
|
|
)
|
|
.await
|
|
.inspect_err(|e| {
|
|
error!(
|
|
old_room_id=?body.room_id,
|
|
new_room_id=?replacement_room_id.as_deref().unwrap(),
|
|
%e,
|
|
"failed to update parent spaces during room upgrade"
|
|
);
|
|
})
|
|
.ok();
|
|
|
|
// MSC4168: Update child rooms to point at the new space, where possible
|
|
debug!("Updating space children");
|
|
update_children(
|
|
&services,
|
|
sender_user,
|
|
&body.room_id,
|
|
replacement_room_id.as_deref().unwrap(),
|
|
)
|
|
.await
|
|
.inspect_err(|e| {
|
|
error!(
|
|
old_room_id=?body.room_id,
|
|
new_room_id=?replacement_room_id.as_deref().unwrap(),
|
|
%e,
|
|
"failed to update space children during room upgrade"
|
|
);
|
|
})
|
|
.ok();
|
|
|
|
// Return the replacement room id
|
|
Ok(upgrade_room::v3::Response::new(replacement_room_id.unwrap()))
|
|
}
|