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