From a2f6141f4baab66cb4d201f2ae24c6d218c31b3c Mon Sep 17 00:00:00 2001 From: Ginger Date: Fri, 10 Apr 2026 12:52:42 -0400 Subject: [PATCH] refactor: Fix errors in `api/client/room/` --- src/api/client/membership/invite.rs | 2 +- src/api/client/room/create.rs | 73 +++++-- src/api/client/room/event.rs | 2 +- src/api/client/room/initial_sync.rs | 39 ++-- src/api/client/room/upgrade.rs | 195 ++++++++++--------- src/service/rooms/state_accessor/user_can.rs | 5 +- 6 files changed, 181 insertions(+), 135 deletions(-) diff --git a/src/api/client/membership/invite.rs b/src/api/client/membership/invite.rs index 23c466581..1d8a3f2a5 100644 --- a/src/api/client/membership/invite.rs +++ b/src/api/client/membership/invite.rs @@ -259,7 +259,7 @@ pub(crate) async fn invite_helper( .rooms .timeline .build_and_append_pdu( - PduBuilder::state(recipient_user.to_string(), &content), + PartialPdu::state(recipient_user.to_string(), &content), sender_user, Some(room_id), &state_lock, diff --git a/src/api/client/room/create.rs b/src/api/client/room/create.rs index 12cab7533..9b0adf074 100644 --- a/src/api/client/room/create.rs +++ b/src/api/client/room/create.rs @@ -9,8 +9,10 @@ use conduwuit::{ use conduwuit_service::{Services, appservice::RegistrationInfo}; use futures::FutureExt; use ruma::{ - CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId, + CanonicalJsonObject, Int, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedRoomId, + OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId, api::client::room::{self, create_room}, + assign, events::{ TimelineEventType, room::{ @@ -26,7 +28,7 @@ use ruma::{ }, }, int, - room_version_rules::RoomIdFormatVersion, + room_version_rules::{AuthorizationRules, RoomIdFormatVersion}, serde::{JsonObject, Raw}, }; use ruminuwuity::invite_permission_config::FilterLevel; @@ -214,18 +216,33 @@ pub(crate) async fn create_room_route( .short .get_or_create_shortroomid(&room_id) .await; - services.rooms.state.mutex.lock(&room_id).await + services.rooms.state.mutex.lock(room_id.as_str()).await }, | None => { - let temp_room_id = RoomId::new(services.globals.server_name()); + let temp_room_id = RoomId::new_v1(services.globals.server_name()); trace!("Locking temporary room state mutex for {temp_room_id}"); - services.rooms.state.mutex.lock(&temp_room_id).await + services.rooms.state.mutex.lock(temp_room_id.as_str()).await }, }; // 1. The room create event debug!("Creating room create event for {sender_user} in room {room_id:?}"); let tmp_id = room_id.as_deref(); + + // Allow requesters to override the `origin_server_ts` to customize room ids + // from v12 onwards + let custom_origin_server_ts = body + .json_body + .as_ref() + .unwrap() + .as_object() + .unwrap() + .get("origin_server_ts") + .and_then(|value| value.as_integer()) + .map(|value| value.into()) + .and_then(|value: i64| value.try_into().ok()) + .map(MilliSecondsSinceUnixEpoch); + let create_event_id = services .rooms .timeline @@ -234,7 +251,7 @@ pub(crate) async fn create_room_route( event_type: TimelineEventType::RoomCreate, content: to_raw_value(&create_content)?, state_key: Some(StateKey::new()), - timestamp: body.origin_server_ts, + timestamp: custom_origin_server_ts, ..Default::default() }, sender_user, @@ -295,7 +312,10 @@ pub(crate) async fn create_room_route( let mut creators: Vec = vec![sender_user.to_owned()]; // Do we care about additional_creators? - if room_version_rules.explicitly_privilege_room_creators { + if room_version_rules + .authorization + .explicitly_privilege_room_creators + { // Have they been specified? if let Some(additional_creators) = create_content.get("additional_creators") { // Are they a real array? @@ -305,9 +325,9 @@ pub(crate) async fn create_room_route( // Are they a string? if let Some(creator) = creator.as_str() { // Do they parse into a real user ID? - if let Ok(creator) = OwnedUserId::parse(creator) { + if let Ok(creator) = UserId::parse(creator) { // Add them to the power levels and creators - creators.push(creator.clone()); + creators.push(creator); } } } @@ -320,10 +340,13 @@ pub(crate) async fn create_room_route( } let power_levels_content = default_power_levels_content( - body.power_level_content_override.as_ref(), + body.power_level_content_override + .as_ref() + .map(|power_levels| power_levels.cast_ref()), &body.visibility, power_levels_to_grant, creators, + &room_version_rules.authorization, )?; services @@ -349,10 +372,13 @@ pub(crate) async fn create_room_route( .rooms .timeline .build_and_append_pdu( - PartialPdu::state(String::new(), &RoomCanonicalAliasEventContent { - alias: Some(room_alias_id.to_owned()), - alt_aliases: vec![], - }), + PartialPdu::state( + String::new(), + &assign!(RoomCanonicalAliasEventContent::new(), { + alias: Some(room_alias_id.to_owned()), + alt_aliases: vec![], + }), + ), sender_user, Some(&room_id), &state_lock, @@ -420,9 +446,11 @@ pub(crate) async fn create_room_route( // 6. Events listed in initial_state for event in &body.initial_state { - let mut partial_pdu = event.deserialize_as::().map_err(|e| { - err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}")))) - })?; + let mut partial_pdu = event + .deserialize_as_unchecked::() + .map_err(|e| { + err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}")))) + })?; debug_info!("Room creation initial state event: {event:?}"); @@ -473,7 +501,7 @@ pub(crate) async fn create_room_route( .rooms .timeline .build_and_append_pdu( - PartialPdu::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }), + PartialPdu::state(String::new(), &RoomTopicEventContent::new(topic.clone())), sender_user, Some(&room_id), &state_lock, @@ -528,10 +556,13 @@ fn default_power_levels_content( visibility: &room::Visibility, users: BTreeMap, creators: Vec, + authorization_rules: &AuthorizationRules, ) -> Result { let mut power_levels_content = - serde_json::to_value(RoomPowerLevelsEventContent { users, ..Default::default() }) - .expect("event is valid, we just created it"); + serde_json::to_value(assign!(RoomPowerLevelsEventContent::new(authorization_rules), { + users + })) + .unwrap(); // secure proper defaults of sensitive/dangerous permissions that moderators // (power level 50) should not have easy access to @@ -621,7 +652,7 @@ async fn room_alias_check( } let server_name = services.globals.server_name(); - let full_room_alias = OwnedRoomAliasId::parse(format!("#{room_alias_name}:{server_name}")) + let full_room_alias = RoomAliasId::parse(format!("#{room_alias_name}:{server_name}")) .map_err(|e| { err!(Request(InvalidParam(debug_error!( ?e, diff --git a/src/api/client/room/event.rs b/src/api/client/room/event.rs index d16888fc1..b592dcf52 100644 --- a/src/api/client/room/event.rs +++ b/src/api/client/room/event.rs @@ -44,5 +44,5 @@ pub(crate) async fn get_room_event_route( event.set_unsigned(body.sender_user.as_deref()); - Ok(get_room_event::v3::Response { event: event.into_format() }) + Ok(get_room_event::v3::Response::new(event.into_format())) } diff --git a/src/api/client/room/initial_sync.rs b/src/api/client/room/initial_sync.rs index 7d919d60e..f64a9ba50 100644 --- a/src/api/client/room/initial_sync.rs +++ b/src/api/client/room/initial_sync.rs @@ -4,7 +4,10 @@ use conduwuit::{ utils::{BoolExt, stream::TryTools}, }; use futures::{FutureExt, TryStreamExt, future::try_join4}; -use ruma::api::client::room::initial_sync::v3::{PaginationChunk, Request, Response}; +use ruma::{ + api::client::peeking::get_current_state::v3::{PaginationChunk, Request, Response}, + assign, +}; use crate::Ruma; @@ -69,29 +72,27 @@ pub(crate) async fn room_initial_sync_route( .boxed() .await?; - let messages = PaginationChunk { - start: events.last().map(at!(0)).as_ref().map(ToString::to_string), + let end = events + .first() + .map(at!(0)) + .as_ref() + .map(ToString::to_string) + .unwrap_or_default(); + let start = events.last().map(at!(0)).as_ref().map(ToString::to_string); - end: events - .first() - .map(at!(0)) - .as_ref() - .map(ToString::to_string) - .unwrap_or_default(), + let chunk = events + .into_iter() + .map(at!(1)) + .map(Event::into_format) + .collect(); - chunk: events - .into_iter() - .map(at!(1)) - .map(Event::into_format) - .collect(), - }; + let messages = assign!(PaginationChunk::new(chunk, end), { start }); - Ok(Response { - room_id: room_id.to_owned(), - account_data: None, + Ok(assign!(Response::new(room_id.to_owned()), { + account_data: vec![], state: state.into(), messages: messages.chunk.is_empty().or_some(messages), visibility: visibility.into(), membership, - }) + })) } diff --git a/src/api/client/room/upgrade.rs b/src/api/client/room/upgrade.rs index 7202744f4..8aa66baae 100644 --- a/src/api/client/room/upgrade.rs +++ b/src/api/client/room/upgrade.rs @@ -2,16 +2,18 @@ use std::cmp::max; use axum::extract::State; use conduwuit::{ - Err, Error, Event, Result, RoomVersion, debug, err, info, + Err, Error, Event, Result, debug, err, info, matrix::{StateKey, pdu::PartialPdu}, }; use futures::{FutureExt, StreamExt}; use ruma::{ CanonicalJsonObject, RoomId, RoomVersionId, - api::client::{error::ErrorKind, room::upgrade_room}, + api::{client::room::upgrade_room, error::ErrorKind}, + assign, events::{ StateEventType, TimelineEventType, room::{ + create::PreviousRoom, member::{MembershipState, RoomMemberEventContent}, power_levels::RoomPowerLevelsEventContent, tombstone::RoomTombstoneEventContent, @@ -19,6 +21,7 @@ use ruma::{ space::child::{RedactedSpaceChildEventContent, SpaceChildEventContent}, }, int, + room_version_rules::RoomIdFormatVersion, }; use serde_json::{json, value::to_raw_value}; @@ -76,7 +79,7 @@ pub(crate) async fn upgrade_room_route( // First, check if the user has permission to upgrade the room (send tombstone // event) - let old_room_state_lock = services.rooms.state.mutex.lock(&body.room_id).await; + 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 // Note that this does internally call the policy server with a fake room ID, @@ -85,10 +88,13 @@ pub(crate) async fn upgrade_room_route( .rooms .timeline .create_hash_and_sign_event( - PartialPdu::state(StateKey::new(), &RoomTombstoneEventContent { - body: "This room has been replaced".to_owned(), - replacement_room: RoomId::new(services.globals.server_name()), - }), + 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, @@ -102,16 +108,19 @@ pub(crate) async fn upgrade_room_route( drop(old_room_state_lock); // Create a replacement room - let room_features = RoomVersion::new(&body.new_version)?; - let replacement_room_owned = if !room_features.room_ids_as_hashes { - Some(RoomId::new(services.globals.server_name())) + let room_version_rules = body + .new_version + .rules() + .expect("new room version should have defined rules"); + let replacement_room_owned = if room_version_rules.room_id_format == RoomIdFormatVersion::V2 { + Some(RoomId::new_v1(services.globals.server_name())) } else { None }; let replacement_room: Option<&RoomId> = replacement_room_owned.as_ref().map(AsRef::as_ref); let replacement_room_tmp = match replacement_room { | Some(v) => v, - | None => &RoomId::new(services.globals.server_name()), + | None => &RoomId::new_v1(services.globals.server_name()), }; let _short_id = services @@ -121,18 +130,21 @@ pub(crate) async fn upgrade_room_route( .await; // For pre-v12 rooms, send tombstone before creating replacement room - let tombstone_event_id = if !room_features.room_ids_as_hashes { - let state_lock = services.rooms.state.mutex.lock(&body.room_id).await; + let tombstone_event_id = if room_version_rules.room_id_format != RoomIdFormatVersion::V2 { + let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await; // Send a m.room.tombstone event to the old room to indicate that it is not // intended to be used any further let tombstone_event_id = services .rooms .timeline .build_and_append_pdu( - PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent { - body: "This room has been replaced".to_owned(), - replacement_room: replacement_room.unwrap().to_owned(), - }), + PartialPdu::state( + StateKey::new(), + &RoomTombstoneEventContent::new( + "This room has been replaced".to_owned(), + replacement_room.unwrap().to_owned(), + ), + ), sender_user, Some(&body.room_id), &state_lock, @@ -145,7 +157,12 @@ pub(crate) async fn upgrade_room_route( } else { None }; - let state_lock = services.rooms.state.mutex.lock(replacement_room_tmp).await; + let state_lock = services + .rooms + .state + .mutex + .lock(replacement_room_tmp.as_str()) + .await; // Get the old room creation event let mut create_event_content: CanonicalJsonObject = services @@ -156,10 +173,13 @@ pub(crate) async fn upgrade_room_route( .map_err(|_| err!(Database("Found room without m.room.create event.")))?; // Use the m.room.tombstone event as the predecessor - let predecessor = Some(ruma::events::room::create::PreviousRoom::new( - body.room_id.clone(), - tombstone_event_id, - )); + + let predecessor = { + #[allow(deprecated, reason = "Clients still use event_id even though it's deprecated")] + Some(assign!(PreviousRoom::new(body.room_id.clone()), { + event_id: tombstone_event_id, + })) + }; // Send a m.room.create event containing a predecessor field and the applicable // room_version @@ -211,7 +231,7 @@ pub(crate) async fn upgrade_room_route( .rooms .timeline .build_and_append_pdu( - PduBuilder { + PartialPdu { event_type: TimelineEventType::RoomCreate, content: to_raw_value(&create_event_content) .expect("event is valid, we just created it"), @@ -227,39 +247,35 @@ pub(crate) async fn upgrade_room_route( .boxed() .await?; let create_id = create_event_id.as_str().replace('$', "!"); - let (replacement_room, state_lock) = if room_features.room_ids_as_hashes { - let parsed_room_id = RoomId::parse(&create_id)?; - (Some(parsed_room_id), services.rooms.state.mutex.lock(parsed_room_id).await) - } else { - (replacement_room, state_lock) - }; + let (replacement_room, state_lock) = + if room_version_rules.room_id_format == RoomIdFormatVersion::V2 { + let parsed_room_id = RoomId::parse(&create_id)?; + let lock = services + .rooms + .state + .mutex + .lock(parsed_room_id.as_str()) + .await; + (Some(parsed_room_id), lock) + } else { + (replacement_room.map(ToOwned::to_owned), state_lock) + }; // Join the new room services .rooms .timeline .build_and_append_pdu( - PduBuilder { - event_type: TimelineEventType::RoomMember, - content: to_raw_value(&RoomMemberEventContent { - membership: MembershipState::Join, + 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(), - is_direct: None, - third_party_invite: None, blurhash: services.users.blurhash(sender_user).await.ok(), - reason: None, - join_authorized_via_users_server: None, - redact_events: None, - }) - .expect("event is valid, we just created it"), - unsigned: None, - state_key: Some(sender_user.as_str().into()), - redacts: None, - timestamp: None, - }, + }), + ), sender_user, - replacement_room, + replacement_room.as_deref(), &state_lock, ) .boxed() @@ -306,14 +322,14 @@ pub(crate) async fn upgrade_room_route( .rooms .timeline .build_and_append_pdu( - PduBuilder { + PartialPdu { event_type: event_type.to_string().into(), content: event_content, state_key: Some(StateKey::from(state_key)), ..Default::default() }, sender_user, - replacement_room, + replacement_room.as_deref(), &state_lock, ) .boxed() @@ -332,27 +348,27 @@ pub(crate) async fn upgrade_room_route( services .rooms .alias - .remove_alias(alias, sender_user) + .remove_alias(&alias, sender_user) .await?; - services - .rooms - .alias - .set_alias(alias, replacement_room.unwrap(), sender_user)?; + services.rooms.alias.set_alias( + &alias, + replacement_room.as_ref().unwrap(), + sender_user, + )?; } // Get the old room power levels - let power_levels_event_content: RoomPowerLevelsEventContent = services + let mut power_levels = services .rooms .state_accessor - .room_state_get_content(&body.room_id, &StateEventType::RoomPowerLevels, "") - .await - .map_err(|_| err!(Database("Found room without m.room.power_levels event.")))?; + .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_event_content + power_levels .users_default .checked_add(int!(1)) .ok_or_else(|| { @@ -360,17 +376,19 @@ pub(crate) async fn upgrade_room_route( })?, ); + power_levels.events_default = new_level; + power_levels.invite = new_level; + // Modify the power levels in the old room to prevent sending of events and // inviting new users services .rooms .timeline .build_and_append_pdu( - PduBuilder::state(StateKey::new(), &RoomPowerLevelsEventContent { - events_default: new_level, - invite: new_level, - ..power_levels_event_content - }), + PartialPdu::state( + StateKey::new(), + &RoomPowerLevelsEventContent::try_from(power_levels).unwrap(), + ), sender_user, Some(&body.room_id), &state_lock, @@ -381,18 +399,21 @@ pub(crate) async fn upgrade_room_route( drop(state_lock); // For v12 rooms, send tombstone AFTER creating replacement room - if room_features.room_ids_as_hashes { - let old_room_state_lock = services.rooms.state.mutex.lock(&body.room_id).await; + if room_version_rules.room_id_format == RoomIdFormatVersion::V2 { + let old_room_state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await; // For v12 rooms, no event reference in predecessor due to cyclic dependency - // could best effort one maybe? services .rooms .timeline .build_and_append_pdu( - PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent { - body: "This room has been replaced".to_owned(), - replacement_room: replacement_room.unwrap().to_owned(), - }), + PartialPdu::state( + StateKey::new(), + &RoomTombstoneEventContent::new( + "This room has been replaced".to_owned(), + replacement_room.as_ref().unwrap().to_owned(), + ), + ), sender_user, Some(&body.room_id), &old_room_state_lock, @@ -415,7 +436,7 @@ pub(crate) async fn upgrade_room_route( .rooms .state_accessor .room_state_get_content::( - space_id, + &space_id, &StateEventType::SpaceChild, body.room_id.as_str(), ) @@ -427,24 +448,24 @@ pub(crate) async fn upgrade_room_route( debug!( "Updating space {space_id} child event for room {} to {}", &body.room_id, - replacement_room.unwrap() + replacement_room.as_ref().unwrap() ); // First, drop the space's child event - let state_lock = services.rooms.state.mutex.lock(space_id).await; + let state_lock = services.rooms.state.mutex.lock(space_id.as_str()).await; debug!("Removing space child event for room {} in space {space_id}", &body.room_id); services .rooms .timeline .build_and_append_pdu( - PduBuilder { + PartialPdu { event_type: StateEventType::SpaceChild.into(), - content: to_raw_value(&RedactedSpaceChildEventContent {}) + content: to_raw_value(&RedactedSpaceChildEventContent::new()) .expect("event is valid, we just created it"), state_key: Some(body.room_id.clone().as_str().into()), ..Default::default() }, sender_user, - Some(space_id), + Some(&space_id), &state_lock, ) .boxed() @@ -453,25 +474,21 @@ pub(crate) async fn upgrade_room_route( // Now, add a new child event for the replacement room debug!( "Adding space child event for room {} in space {space_id}", - replacement_room.unwrap() + replacement_room.as_ref().unwrap() ); 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()], + PartialPdu::state( + replacement_room.as_ref().unwrap().as_str(), + &assign!(SpaceChildEventContent::new(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.unwrap().as_str().into()), - ..Default::default() - }, + }), + ), sender_user, - Some(space_id), + Some(&space_id), &state_lock, ) .boxed() @@ -480,13 +497,11 @@ pub(crate) async fn upgrade_room_route( debug!( "Finished updating space {space_id} child event for room {} to {}", &body.room_id, - replacement_room.unwrap() + replacement_room.as_ref().unwrap() ); drop(state_lock); } // Return the replacement room id - Ok(upgrade_room::v3::Response { - replacement_room: replacement_room.unwrap().to_owned(), - }) + Ok(upgrade_room::v3::Response::new(replacement_room.as_ref().unwrap().to_owned())) } diff --git a/src/service/rooms/state_accessor/user_can.rs b/src/service/rooms/state_accessor/user_can.rs index 9ff601360..fb74b27ad 100644 --- a/src/service/rooms/state_accessor/user_can.rs +++ b/src/service/rooms/state_accessor/user_can.rs @@ -4,7 +4,6 @@ use ruma::{ events::{ StateEventType, TimelineEventType, room::{ - create::RoomCreateEventContent, history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, member::{MembershipState, RoomMemberEventContent}, }, @@ -64,7 +63,7 @@ pub async fn user_can_redact( return Ok(is_own_event); } - return Ok(false); + Ok(false) } /// Whether a user is allowed to see an event, based on @@ -139,7 +138,7 @@ pub async fn user_can_invite( self.services .timeline .create_hash_and_sign_event( - PduBuilder::state( + PartialPdu::state( target_user.as_str(), &RoomMemberEventContent::new(MembershipState::Invite), ),