Skip to main content

tuwunel_api_client/room/
upgrade.rs

1use std::{cmp::max, iter::once};
2
3use axum::extract::State;
4use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt};
5use ruma::{
6	CanonicalJsonObject, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId, UserId,
7	api::client::room::upgrade_room::v3,
8	events::{
9		StateEventType, TimelineEventType,
10		room::{
11			create::{PreviousRoom, RoomCreateEventContent},
12			member::{MembershipState, RoomMemberEventContent},
13			power_levels::RoomPowerLevelsEventContent,
14			tombstone::RoomTombstoneEventContent,
15		},
16	},
17	int,
18	room::RoomType,
19	room_version_rules::{RoomIdFormatVersion, RoomVersionRules},
20};
21use serde_json::{Value as JsonValue, json, value::to_raw_value};
22use tuwunel_core::{
23	Err, Result, debug_info, err, error, implement, info, is_equal_to, is_less_than,
24	itertools::Itertools,
25	utils::{
26		ReadyExt,
27		future::TryExtExt,
28		stream::{IterStream, TryIgnore, WidebandExt},
29	},
30};
31use tuwunel_matrix::{Event, StateKey, pdu::PduBuilder, room_version};
32use tuwunel_service::{Services, rooms::timeline::RoomMutexGuard};
33
34use crate::Ruma;
35
36//TODO: Upgrade Ruma
37const RECOMMENDED_TRANSFERABLE_STATE_EVENT_TYPES: &[StateEventType; 9] = &[
38	StateEventType::RoomServerAcl,
39	StateEventType::RoomEncryption,
40	StateEventType::RoomName,
41	StateEventType::RoomAvatar,
42	StateEventType::RoomTopic,
43	StateEventType::RoomGuestAccess,
44	StateEventType::RoomHistoryVisibility,
45	StateEventType::RoomJoinRules,
46	StateEventType::RoomPowerLevels,
47];
48
49const RTC_SLOT_TYPES: [&str; 2] = ["m.rtc.slot", "org.matrix.msc4143.rtc.slot"];
50
51#[derive(Debug)]
52struct RoomUpgradeContext<'a> {
53	services: &'a Services,
54	sender_user: &'a UserId,
55	creator: &'a UserId,
56	old_room_id: &'a RoomId,
57	old_state_lock: &'a RoomMutexGuard,
58	old_version_rules: &'a RoomVersionRules,
59	new_room_id: &'a RoomId,
60	new_state_lock: &'a RoomMutexGuard,
61	new_version_rules: &'a RoomVersionRules,
62	additional_creators: &'a [OwnedUserId],
63}
64
65/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
66///
67/// Upgrades the room.
68///
69/// - Creates a replacement room
70/// - Sends a tombstone event into the current room
71/// - Sender user joins the room
72/// - Transfers some state events
73/// - Moves local aliases
74/// - Modifies old room power levels to prevent users from speaking
75#[tracing::instrument(level = "debug")]
76pub(crate) async fn upgrade_room_route(
77	State(services): State<crate::State>,
78	body: Ruma<v3::Request>,
79) -> Result<v3::Response> {
80	let sender_user = body.sender_user();
81	let new_version = &body.new_version;
82	let version_rules = room_version::rules(new_version)?;
83
84	if !services
85		.config
86		.supported_room_version(new_version)
87	{
88		return Err!(Request(UnsupportedRoomVersion(
89			"This server does not support that room version.",
90		)));
91	}
92
93	let old_room_id = &body.room_id;
94	let old_state_lock = services.state.mutex.lock(old_room_id).await;
95
96	if !services
97		.state_accessor
98		.user_can_tombstone(old_room_id, sender_user, &old_state_lock)
99		.await
100	{
101		return Err!(Request(Forbidden("You are not permitted to upgrade the room.")));
102	}
103
104	let latest_event = services
105		.timeline
106		.latest_pdu_in_room(old_room_id)
107		.await
108		.ok();
109
110	let predecessor = PreviousRoom {
111		room_id: old_room_id.to_owned(),
112		event_id: latest_event
113			.as_ref()
114			.map(Event::event_id)
115			.map(ToOwned::to_owned),
116	};
117
118	debug_info!(
119		%sender_user,
120		%old_room_id,
121		last_event = ?predecessor.event_id,
122		?new_version,
123		"Attempting upgrade of room..."
124	);
125
126	let creator = if services.admin.is_admin_room(&body.room_id).await {
127		&services.globals.server_user
128	} else {
129		sender_user
130	};
131
132	let (replacement_room, state_lock) = match version_rules.room_id_format {
133		| RoomIdFormatVersion::V2 =>
134			upgrade_room_create(
135				&services,
136				creator,
137				old_room_id,
138				new_version,
139				&version_rules,
140				predecessor,
141				body.additional_creators.clone(),
142			)
143			.await,
144
145		| RoomIdFormatVersion::V1 =>
146			upgrade_room_create_legacy(
147				&services,
148				creator,
149				old_room_id,
150				new_version,
151				&version_rules,
152				predecessor,
153			)
154			.await,
155	}
156	.inspect_err(|e| error!(?body, "Upgrade m.room.create event failed: {e}"))?;
157
158	let old_room_id = &body.room_id;
159	let old_version = services
160		.state
161		.get_room_version(old_room_id)
162		.await?;
163	let old_version_rules = room_version::rules(&old_version)?;
164
165	let context = RoomUpgradeContext {
166		services: &services,
167		sender_user,
168		creator,
169		old_room_id,
170		old_state_lock: &old_state_lock,
171		old_version_rules: &old_version_rules,
172		new_room_id: &replacement_room,
173		new_state_lock: &state_lock,
174		new_version_rules: &version_rules,
175		additional_creators: &body.additional_creators,
176	};
177
178	if let Err(e) = context.transfer_room().await {
179		error!(?e, ?context, "Room upgrade failed. Cleaning up incomplete room...");
180
181		if let Err(e) = services
182			.delete
183			.delete_room(&replacement_room, false, state_lock)
184			.await
185		{
186			error!("Additional errors while deleting incomplete room: {e}");
187		}
188
189		return Err(e);
190	}
191
192	info!(
193		old_room_id = %context.old_room_id,
194		new_room_id = %context.new_room_id,
195		upgraded_by = %sender_user,
196		"Room upgraded",
197	);
198
199	Ok(v3::Response { replacement_room })
200}
201
202#[tracing::instrument(level = "info")]
203async fn upgrade_room_create(
204	services: &Services,
205	sender_user: &UserId,
206	old_room_id: &RoomId,
207	new_version: &RoomVersionId,
208	version_rules: &RoomVersionRules,
209	predecessor: PreviousRoom,
210	additional_creators: Vec<OwnedUserId>,
211) -> Result<(OwnedRoomId, RoomMutexGuard)> {
212	// Get the old room creation event
213	let mut content: CanonicalJsonObject = services
214		.state_accessor
215		.room_state_get_content(old_room_id, &StateEventType::RoomCreate, "")
216		.await
217		.map_err(|_| err!(Database("Found room without m.room.create event.")))?;
218
219	content.remove("creator");
220	// MSC4291: v12 create events omit the deprecated predecessor.event_id.
221	let predecessor = PreviousRoom { event_id: None, ..predecessor };
222
223	content.insert("predecessor".into(), json!(predecessor).try_into()?);
224	content.insert("room_version".into(), json!(new_version).try_into()?);
225
226	if version_rules
227		.authorization
228		.additional_room_creators
229	{
230		let additional_creators = additional_creators
231			.into_iter()
232			.sorted()
233			.dedup()
234			.collect_vec();
235
236		content.remove("additional_creators");
237		if !additional_creators.is_empty() {
238			content.insert("additional_creators".into(), json!(additional_creators).try_into()?);
239		}
240	}
241
242	// Validate creation event content
243	let raw_content = to_raw_value(&content)?;
244	if let Err(e) = serde_json::from_str::<CanonicalJsonObject>(raw_content.get()) {
245		return Err!(Request(BadJson("Error forming creation event: {e}")));
246	}
247
248	let create_event_id = services
249		.timeline
250		.build_and_append_create_pdu(content, sender_user, None, None)
251		.boxed() // size firewall
252		.await?;
253
254	// The real room_id is now the event_id.
255	let room_id = OwnedRoomId::from_parts('!', create_event_id.localpart(), None)?;
256	let state_lock = services.state.mutex.lock(&room_id).await;
257
258	Ok((room_id, state_lock))
259}
260
261#[tracing::instrument(level = "info")]
262async fn upgrade_room_create_legacy(
263	services: &Services,
264	sender_user: &UserId,
265	old_room_id: &RoomId,
266	new_version: &RoomVersionId,
267	version_rules: &RoomVersionRules,
268	predecessor: PreviousRoom,
269) -> Result<(OwnedRoomId, RoomMutexGuard)> {
270	// Create a replacement room
271	let new_room_id = RoomId::new_v1(services.globals.server_name());
272	let state_lock = services.state.mutex.lock(&new_room_id).await;
273	let _short_id = services
274		.short
275		.get_or_create_shortroomid(&new_room_id)
276		.await;
277
278	// Get the old room creation event
279	let mut content: CanonicalJsonObject = services
280		.state_accessor
281		.room_state_get_content(old_room_id, &StateEventType::RoomCreate, "")
282		.await
283		.map_err(|_| err!(Database("Found room without m.room.create event.")))?;
284
285	// Send a m.room.create event containing a predecessor field and the applicable
286	// room_version. "creator" key no longer exists in V11+ rooms.
287	if !version_rules.authorization.use_room_create_sender {
288		content.insert("creator".into(), json!(&sender_user).try_into()?);
289	} else {
290		content.remove("creator");
291	}
292
293	content.insert("predecessor".into(), json!(predecessor).try_into()?);
294	content.insert("room_version".into(), json!(new_version).try_into()?);
295
296	// Validate creation event content
297	let raw_content = to_raw_value(&content)?;
298	if let Err(e) = serde_json::from_str::<CanonicalJsonObject>(raw_content.get()) {
299		return Err!(Request(BadJson("Error forming creation event: {e}")));
300	}
301
302	services
303		.timeline
304		.build_and_append_pdu(
305			PduBuilder {
306				event_type: TimelineEventType::RoomCreate,
307				content: to_raw_value(&content)?.into(),
308				state_key: Some(StateKey::new()),
309				..Default::default()
310			},
311			sender_user,
312			&new_room_id,
313			&state_lock,
314		)
315		.await?;
316
317	Ok((new_room_id, state_lock))
318}
319
320#[implement(RoomUpgradeContext, params = "<'_>")]
321#[tracing::instrument(level = "debug")]
322async fn transfer_room(&self) -> Result {
323	self.move_creator().await?;
324
325	self.move_state_events().await?;
326
327	self.move_space_state().await?;
328
329	self.move_rtc_slots().await?;
330
331	self.move_sender_user().await?;
332
333	self.move_push_rules().await?;
334
335	self.move_local_aliases().await?;
336
337	self.tombstone_old_room().await?;
338
339	// After commitment to the tombstone above no more errors can propagate.
340	self.lockdown_old_room()
341		.await
342		.inspect_err(|e| error!(?self, "Failed to lockdown old room: {e}"))
343		.ok();
344
345	Ok(())
346}
347
348// Join the new room
349#[implement(RoomUpgradeContext, params = "<'_>")]
350#[tracing::instrument(level = "debug")]
351async fn move_creator(&self) -> Result {
352	self.move_member(self.creator).await?;
353
354	Ok(())
355}
356
357#[implement(RoomUpgradeContext, params = "<'_>")]
358#[tracing::instrument(level = "debug")]
359async fn move_sender_user(&self) -> Result {
360	if self.sender_user != self.creator {
361		self.services
362			.timeline
363			.build_and_append_pdu(
364				PduBuilder::state(
365					self.sender_user.as_str(),
366					&RoomMemberEventContent::new(MembershipState::Invite),
367				),
368				self.creator,
369				self.new_room_id,
370				self.new_state_lock,
371			)
372			.await?;
373
374		self.move_member(self.sender_user).await?;
375	}
376
377	Ok(())
378}
379
380#[implement(RoomUpgradeContext, params = "<'_>")]
381#[tracing::instrument(level = "debug")]
382async fn move_push_rules(&self) -> Result {
383	self.services
384		.account_data
385		.copy_room_push_rule(self.sender_user, self.old_room_id, self.new_room_id)
386		.await
387}
388
389#[implement(RoomUpgradeContext, params = "<'_>")]
390#[tracing::instrument(level = "debug")]
391async fn move_member(&self, user_id: &UserId) -> Result {
392	let old_content: RoomMemberEventContent = self
393		.services
394		.state_accessor
395		.room_state_get_content(self.old_room_id, &StateEventType::RoomMember, user_id.as_str())
396		.inspect_err(|e| error!(?self, "Missing room member event: {e}"))
397		.await?;
398
399	self.services
400		.timeline
401		.build_and_append_pdu(
402			PduBuilder::state(user_id.as_str(), &RoomMemberEventContent {
403				membership: MembershipState::Join,
404				join_authorized_via_users_server: None,
405				..old_content
406			}),
407			user_id,
408			self.new_room_id,
409			self.new_state_lock,
410		)
411		.await?;
412
413	Ok(())
414}
415
416// Replicate transferable state events to the new room
417#[implement(RoomUpgradeContext, params = "<'_>")]
418#[tracing::instrument(level = "debug")]
419async fn move_state_events(&self) -> Result {
420	RECOMMENDED_TRANSFERABLE_STATE_EVENT_TYPES
421		.iter()
422		.rev()
423		.stream()
424		.wide_filter_map(|event_type| {
425			self.services
426				.state_accessor
427				.room_state_get(self.old_room_id, event_type, "")
428				.ok()
429		})
430		.map(Ok)
431		.try_for_each(async |event| {
432			self.services
433				.timeline
434				.build_and_append_pdu(
435					self.rebuild_state_event(&event).await?,
436					self.creator,
437					self.new_room_id,
438					self.new_state_lock,
439				)
440				.inspect_err(|e| {
441					error!(?event, ?self, "Failed to transfer state on upgrade: {e}");
442				})
443				.map_ok(|_| ())
444				.await
445		})
446		.await
447}
448
449// MSC4168: copy m.space.parent for any room, plus m.space.child when the
450// old room is itself a space, into the upgraded room.
451#[implement(RoomUpgradeContext, params = "<'_>")]
452#[tracing::instrument(level = "debug")]
453async fn move_space_state(&self) -> Result {
454	let old_room_is_space = self
455		.services
456		.state_accessor
457		.room_state_get_content::<RoomCreateEventContent>(
458			self.old_room_id,
459			&StateEventType::RoomCreate,
460			"",
461		)
462		.await
463		.ok()
464		.and_then(|c| c.room_type)
465		.is_some_and(|t| matches!(t, RoomType::Space));
466
467	let event_types: &[StateEventType] = match old_room_is_space {
468		| true => &[StateEventType::SpaceParent, StateEventType::SpaceChild],
469		| false => &[StateEventType::SpaceParent],
470	};
471
472	self.move_keyed_state(event_types).await
473}
474
475#[implement(RoomUpgradeContext, params = "<'_>")]
476#[tracing::instrument(level = "debug")]
477async fn move_rtc_slots(&self) -> Result {
478	// MSC4143: copy RTC slots into the upgraded room.
479	self.move_keyed_state(&RTC_SLOT_TYPES.map(StateEventType::from))
480		.await
481}
482
483#[implement(RoomUpgradeContext, params = "<'_>")]
484// try_for_each requires FnMut returning a nameable future; an async closure
485// capturing self does not satisfy it.
486#[expect(closure_returning_async_block)]
487#[tracing::instrument(level = "debug")]
488async fn move_keyed_state(&self, event_types: &[StateEventType]) -> Result {
489	event_types
490		.iter()
491		.stream()
492		.map(Ok)
493		.try_for_each(|event_type| {
494			self.services
495				.state_accessor
496				.room_state_keys(self.old_room_id, event_type)
497				.ignore_err()
498				.map(Ok)
499				.try_for_each(move |state_key| async move {
500					let Ok(event) = self
501						.services
502						.state_accessor
503						.room_state_get(self.old_room_id, event_type, &state_key)
504						.await
505					else {
506						return Ok(());
507					};
508
509					self.services
510						.timeline
511						.build_and_append_pdu(
512							self.rebuild_state_event(&event).await?,
513							self.creator,
514							self.new_room_id,
515							self.new_state_lock,
516						)
517						.inspect_err(|e| {
518							error!(?event, ?self, %e, "Failed to copy keyed state");
519						})
520						.await
521						.ok();
522
523					Ok(())
524				})
525		})
526		.await
527}
528
529#[implement(RoomUpgradeContext, params = "<'_>")]
530#[tracing::instrument(level = "debug")]
531async fn rebuild_state_event<Pdu: Event>(&self, event: &Pdu) -> Result<PduBuilder> {
532	let content = match event.kind() {
533		| TimelineEventType::RoomPowerLevels => {
534			let mut content = event.get_content_as_value();
535
536			if self
537				.new_version_rules
538				.authorization
539				.explicitly_privilege_room_creators
540			{
541				if let Some(users) = content
542					.get_mut("users")
543					.and_then(JsonValue::as_object_mut)
544				{
545					users.retain(|user_id, _pl| {
546						!self
547							.additional_creators
548							.iter()
549							.map(AsRef::as_ref)
550							.chain(once(self.creator))
551							.map(UserId::as_str)
552							.any(is_equal_to!(user_id.as_str()))
553					});
554				}
555
556				if self.creator == self.sender_user
557					&& content["events"]["m.room.tombstone"]
558						.as_i64()
559						.is_none_or(is_less_than!(150))
560				{
561					content["events"]["m.room.tombstone"] = json!(150);
562				}
563			} else if self
564				.old_version_rules
565				.authorization
566				.explicitly_privilege_room_creators
567			{
568				#[expect(clippy::collapsible_if)]
569				if let Some(users) = content
570					.as_object_mut()
571					.expect("power levels event content must be an object")
572					.entry("users")
573					.or_insert(json!({}))
574					.as_object_mut()
575				{
576					let level = json!(1000);
577
578					self.services
579						.state_accessor
580						.get_create(self.old_room_id)
581						.await?
582						.creators(&self.old_version_rules.authorization)?
583						.for_each(|user_id| {
584							users.insert(user_id.to_string(), level.clone());
585						});
586				}
587			}
588
589			to_raw_value(&content)?
590		},
591		// MSC4168: rewrite `via` to the upgrading server's name on copied
592		// space-graph state events, since the previous room's via list may
593		// no longer cover the upgraded room.
594		| TimelineEventType::SpaceChild | TimelineEventType::SpaceParent => {
595			let mut content = event.get_content_as_value();
596			if let Some(obj) = content.as_object_mut() {
597				obj.insert("via".to_owned(), json!([self.sender_user.server_name().as_str()]));
598			}
599
600			to_raw_value(&content)?
601		},
602		| _ => to_raw_value(event.content())?,
603	};
604
605	Ok(PduBuilder {
606		content: content.into(),
607		event_type: event.kind().clone(),
608		state_key: event.state_key().map(Into::into),
609		..Default::default()
610	})
611}
612
613// Moves any local aliases to the new room
614#[implement(RoomUpgradeContext, params = "<'_>")]
615#[tracing::instrument(level = "debug")]
616async fn move_local_aliases(&self) -> Result {
617	self.services
618		.alias
619		.local_aliases_for_room(self.old_room_id)
620		.ready_for_each(|alias| {
621			self.services
622				.alias
623				.set_alias_by(alias, self.new_room_id, self.creator)
624				.inspect_err(|e| error!(?self, "Failed to add alias: {e}"))
625				.ok();
626		})
627		.map(Ok)
628		.await
629}
630
631// Send a m.room.tombstone event to the old room to indicate that it is not
632// intended to be used any further Fail if the sender does not have the required
633// permissions.
634#[implement(RoomUpgradeContext, params = "<'_>")]
635#[tracing::instrument(level = "debug")]
636async fn tombstone_old_room(&self) -> Result<OwnedEventId> {
637	self.services
638		.timeline
639		.build_and_append_pdu(
640			PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
641				body: "This room has been upgraded.".to_owned(),
642				replacement_room: self.new_room_id.to_owned(),
643			}),
644			self.sender_user,
645			self.old_room_id,
646			self.old_state_lock,
647		)
648		.await
649}
650
651// Modify the power levels in the old room to prevent sending of events and
652// inviting new users. Though a Result is returned, the callsite above treats it
653// as infallible because the tombstone represents the commitment.
654#[implement(RoomUpgradeContext, params = "<'_>")]
655#[tracing::instrument(level = "debug")]
656async fn lockdown_old_room(&self) -> Result<OwnedEventId> {
657	// Get the old room power levels
658	let old_content: RoomPowerLevelsEventContent = self
659		.services
660		.state_accessor
661		.room_state_get_content(self.old_room_id, &StateEventType::RoomPowerLevels, "")
662		.await
663		.map_err(|_| err!(Database("Found room without m.room.power_levels event.")))?;
664
665	let old_users_default = old_content
666		.users_default
667		.checked_add(int!(1))
668		.ok_or_else(|| {
669			err!(Request(BadJson("users_default power levels event content is not valid")))
670		})?;
671
672	// Setting events_default and invite to the greater of 50 and users_default + 1
673	let new_level = max(int!(50), old_users_default);
674
675	self.services
676		.timeline
677		.build_and_append_pdu(
678			PduBuilder::state(StateKey::new(), &RoomPowerLevelsEventContent {
679				events_default: new_level,
680				invite: new_level,
681				..old_content
682			}),
683			self.sender_user,
684			self.old_room_id,
685			self.old_state_lock,
686		)
687		.await
688}