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
36const 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#[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 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 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 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() .await?;
253
254 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 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 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 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 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 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#[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#[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#[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 self.move_keyed_state(&RTC_SLOT_TYPES.map(StateEventType::from))
480 .await
481}
482
483#[implement(RoomUpgradeContext, params = "<'_>")]
484#[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 | 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#[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#[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#[implement(RoomUpgradeContext, params = "<'_>")]
655#[tracing::instrument(level = "debug")]
656async fn lockdown_old_room(&self) -> Result<OwnedEventId> {
657 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 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}