Skip to main content

tuwunel_api/client/room/
create.rs

1use std::collections::BTreeMap;
2
3use axum::extract::State;
4use futures::{FutureExt, StreamExt};
5use itertools::Itertools;
6use ruma::{
7	CanonicalJsonObject, EventEncryptionAlgorithm, Int, OwnedRoomAliasId, OwnedRoomId,
8	OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId,
9	api::client::room::{
10		self,
11		create_room::{
12			self, RoomPowerLevelsContentOverride,
13			v3::{CreationContent, RoomPreset},
14		},
15	},
16	events::{
17		StateEventType, TimelineEventType,
18		room::{
19			canonical_alias::RoomCanonicalAliasEventContent,
20			create::{PreviousRoom, RoomCreateEventContent},
21			encryption::RoomEncryptionEventContent,
22			guest_access::{GuestAccess, RoomGuestAccessEventContent},
23			history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
24			join_rules::{JoinRule, RoomJoinRulesEventContent},
25			member::{MembershipState, RoomMemberEventContent},
26			name::RoomNameEventContent,
27			power_levels::RoomPowerLevelsEventContent,
28			topic::RoomTopicEventContent,
29		},
30	},
31	int,
32	room_version_rules::{RoomIdFormatVersion, RoomVersionRules},
33	serde::{JsonObject, Raw},
34};
35use serde::Deserialize;
36use serde_json::{Value as JsonValue, json, value::to_raw_value};
37use tuwunel_core::{
38	Err, Result, debug_info, debug_warn, err, info,
39	matrix::{
40		StateKey,
41		pdu::{Content, PduBuilder},
42		room_version,
43	},
44	utils::{BoolExt, IterStream, ReadyExt, option::OptionExt},
45	warn,
46};
47use tuwunel_service::{Services, appservice::RegistrationInfo, rooms::state::RoomMutexGuard};
48
49use crate::{Ruma, client::utils::invite_check};
50
51pub(crate) async fn create_room_route(
52	State(services): State<crate::State>,
53	body: Ruma<create_room::v3::Request>,
54) -> Result<create_room::v3::Response> {
55	can_create_room_check(&services, &body).await?;
56	can_publish_directory_check(&services, &body).await?;
57
58	// Figure out preset. We need it for preset specific events
59	let preset = body
60		.preset
61		.clone()
62		.unwrap_or(match &body.visibility {
63			| room::Visibility::Public => RoomPreset::PublicChat,
64			| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
65		});
66
67	// Determine room version
68	let (room_version, version_rules) = body
69		.room_version
70		.as_ref()
71		.map_or(Ok(&services.server.config.default_room_version), |version| {
72			services
73				.config
74				.supported_room_version(version)
75				.then_ok_or_else(version, || {
76					err!(Request(UnsupportedRoomVersion(
77						"This server does not support room version {version:?}"
78					)))
79				})
80		})
81		.and_then(|version| Ok((version, room_version::rules(version)?)))?;
82
83	// Error on existing alias before committing to creation.
84	let alias = body
85		.room_alias_name
86		.as_ref()
87		.map_async(|alias| room_alias_check(&services, alias, body.appservice_info.as_ref()))
88		.await
89		.transpose()?;
90
91	// Increment and hold the counter; the room will sync atomically to clients
92	// which is preferable.
93	let next_count = services.globals.next_count();
94
95	// 1. Create the create event.
96	let (room_id, state_lock) = match version_rules.room_id_format {
97		| RoomIdFormatVersion::V1 =>
98			create_create_event_legacy(&services, &body, room_version, &version_rules).await?,
99		| RoomIdFormatVersion::V2 =>
100			create_create_event(&services, &body, &preset, room_version, &version_rules)
101				.await
102				.map_err(|e| {
103					err!(Request(InvalidParam("Error while creating m.room.create event: {e}")))
104				})?,
105	};
106
107	let sender_user = body.sender_user();
108
109	// 2. Let the room creator join
110	apply_creator_join_pdu(&services, &body, sender_user, &room_id, &state_lock)
111		.boxed()
112		.await?;
113
114	// 3. Power levels
115	apply_power_levels_pdu(
116		&services,
117		&body,
118		&preset,
119		&version_rules,
120		sender_user,
121		&room_id,
122		&state_lock,
123	)
124	.boxed()
125	.await?;
126
127	// 4. Canonical room alias
128	if let Some(room_alias_id) = &alias {
129		apply_canonical_alias_pdu(&services, room_alias_id, sender_user, &room_id, &state_lock)
130			.boxed()
131			.await?;
132	}
133
134	// 5. Events set by preset
135	let initial_state =
136		apply_preset_state_pdus(&services, &body, &preset, sender_user, &room_id, &state_lock)
137			.boxed()
138			.await?;
139
140	// 6. Events listed in initial_state
141	apply_initial_state_pdus(
142		&services,
143		initial_state,
144		&preset,
145		sender_user,
146		&room_id,
147		&state_lock,
148	)
149	.boxed()
150	.await?;
151
152	// 7. Events implied by name and topic
153	apply_name_and_topic_pdus(&services, &body, sender_user, &room_id, &state_lock)
154		.boxed()
155		.await?;
156
157	drop(next_count);
158	drop(state_lock);
159
160	// if inviting anyone with room creation and invite check passes
161	if (!body.invite.is_empty() || !body.invite_3pid.is_empty())
162		&& invite_check(&services, sender_user, &room_id)
163			.await
164			.is_ok()
165	{
166		process_invites(&services, &body, sender_user, &room_id)
167			.boxed()
168			.await;
169	}
170
171	finalize_alias_and_directory(&services, &body, alias.as_deref(), sender_user, &room_id)
172		.await?;
173
174	copy_creator_predecessor_push_rule(
175		&services,
176		body.creation_content.as_ref(),
177		sender_user,
178		&room_id,
179	)
180	.await;
181
182	info!("{sender_user} created a room with room ID {room_id}");
183
184	Ok(create_room::v3::Response::new(room_id))
185}
186
187async fn apply_creator_join_pdu(
188	services: &Services,
189	body: &Ruma<create_room::v3::Request>,
190	sender_user: &UserId,
191	room_id: &RoomId,
192	state_lock: &RoomMutexGuard,
193) -> Result {
194	let mut content = RoomMemberEventContent {
195		is_direct: body.is_direct,
196		..RoomMemberEventContent::new(MembershipState::Join)
197	};
198
199	services
200		.profile
201		.fill_profile_data(sender_user, &mut content)
202		.await;
203
204	services
205		.timeline
206		.build_and_append_pdu(
207			PduBuilder::state(sender_user.to_string(), &content),
208			sender_user,
209			room_id,
210			state_lock,
211		)
212		.await
213		.map(|_| ())
214}
215
216async fn apply_power_levels_pdu(
217	services: &Services,
218	body: &Ruma<create_room::v3::Request>,
219	preset: &RoomPreset,
220	version_rules: &RoomVersionRules,
221	sender_user: &UserId,
222	room_id: &RoomId,
223	state_lock: &RoomMutexGuard,
224) -> Result {
225	let users =
226		build_power_levels_users(services, body, preset, version_rules, sender_user).await;
227
228	let default_override = services
229		.config
230		.default_power_level_content_override
231		.as_ref();
232
233	let power_levels_content = default_power_levels_content(
234		version_rules,
235		default_override,
236		body.power_level_content_override.as_ref(),
237		preset,
238		users,
239	)?;
240
241	services
242		.timeline
243		.build_and_append_pdu(
244			PduBuilder {
245				event_type: TimelineEventType::RoomPowerLevels,
246				content: to_raw_value(&power_levels_content)?.into(),
247				state_key: Some(StateKey::new()),
248				..Default::default()
249			},
250			sender_user,
251			room_id,
252			state_lock,
253		)
254		.await
255		.map(|_| ())
256}
257
258async fn build_power_levels_users(
259	services: &Services,
260	body: &Ruma<create_room::v3::Request>,
261	preset: &RoomPreset,
262	version_rules: &RoomVersionRules,
263	sender_user: &UserId,
264) -> BTreeMap<OwnedUserId, Int> {
265	let seed = version_rules
266		.authorization
267		.explicitly_privilege_room_creators
268		.or(|| (sender_user.to_owned(), int!(100)))
269		.into_iter()
270		.collect::<BTreeMap<_, _>>();
271
272	let trusted_invitees = *preset == RoomPreset::TrustedPrivateChat
273		&& !version_rules
274			.authorization
275			.additional_room_creators;
276
277	if !trusted_invitees {
278		return seed;
279	}
280
281	body.invite
282		.iter()
283		.stream()
284		.filter(|&invite| invite_allowed(services, sender_user, invite))
285		.ready_fold(seed, |mut users, invite| {
286			users.insert(invite.clone(), int!(100));
287			users
288		})
289		.await
290}
291
292async fn apply_canonical_alias_pdu(
293	services: &Services,
294	room_alias_id: &RoomAliasId,
295	sender_user: &UserId,
296	room_id: &RoomId,
297	state_lock: &RoomMutexGuard,
298) -> Result {
299	services
300		.timeline
301		.build_and_append_pdu(
302			PduBuilder::state(String::new(), &RoomCanonicalAliasEventContent {
303				alias: Some(room_alias_id.to_owned()),
304				alt_aliases: vec![],
305			}),
306			sender_user,
307			room_id,
308			state_lock,
309		)
310		.await
311		.map(|_| ())
312}
313
314async fn apply_preset_state_pdus(
315	services: &Services,
316	body: &Ruma<create_room::v3::Request>,
317	preset: &RoomPreset,
318	sender_user: &UserId,
319	room_id: &RoomId,
320	state_lock: &RoomMutexGuard,
321) -> Result<Vec<InitialEvent>> {
322	let mut initial_state = body
323		.initial_state
324		.iter()
325		.map(|state| Ok(state.deserialize_as_unchecked::<InitialEvent>()?))
326		.filter_ok(|event| {
327			services.config.allow_encryption || event.event_type != StateEventType::RoomEncryption
328		})
329		.filter_ok(|event| {
330			// client/appservice workaround: if a user sends an initial_state event with a
331			// state event in there with the content of literally `{}` (not null or empty
332			// string), let's just skip it over and warn.
333			if event.content.json().get() == "{}" {
334				debug_warn!("skipping empty initial state event of type {}", event.event_type);
335				false
336			} else {
337				true
338			}
339		})
340		.filter_ok(|event| body.name.is_none() || event.event_type != StateEventType::RoomName)
341		.filter_ok(|event| body.topic.is_none() || event.event_type != StateEventType::RoomTopic)
342		.collect::<Result<Vec<_>>>()?;
343
344	let join_rule_pdubuilder =
345		take_initial(&mut initial_state, &StateEventType::RoomJoinRules, "")
346			.map(Into::into)
347			.unwrap_or_else(|| {
348				PduBuilder::state(
349					String::new(),
350					&RoomJoinRulesEventContent::new(match preset {
351						| RoomPreset::PublicChat => JoinRule::Public,
352						// according to spec "invite" is the default
353						| _ => JoinRule::Invite,
354					}),
355				)
356			});
357
358	let history_visibility_pdubuilder =
359		take_initial(&mut initial_state, &StateEventType::RoomHistoryVisibility, "")
360			.map(Into::into)
361			.unwrap_or_else(|| {
362				PduBuilder::state(
363					String::new(),
364					&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared),
365				)
366			});
367
368	let guest_access = guest_access_pdu(
369		take_initial(&mut initial_state, &StateEventType::RoomGuestAccess, "").map(Into::into),
370		preset,
371	);
372
373	// 5.1 Join Rules
374	services
375		.timeline
376		.build_and_append_pdu(join_rule_pdubuilder, sender_user, room_id, state_lock)
377		.boxed()
378		.await?;
379
380	// 5.2 History Visibility
381	services
382		.timeline
383		.build_and_append_pdu(history_visibility_pdubuilder, sender_user, room_id, state_lock)
384		.boxed()
385		.await?;
386
387	// 5.3 Guest Access
388	if let Some(guest_access) = guest_access {
389		services
390			.timeline
391			.build_and_append_pdu(guest_access, sender_user, room_id, state_lock)
392			.boxed()
393			.await?;
394	}
395
396	Ok(initial_state)
397}
398
399fn guest_access_pdu(initial: Option<PduBuilder>, preset: &RoomPreset) -> Option<PduBuilder> {
400	let can_join = || {
401		PduBuilder::state(String::new(), &RoomGuestAccessEventContent::new(GuestAccess::CanJoin))
402	};
403
404	initial.or_else(|| preset.ne(&RoomPreset::PublicChat).then(can_join))
405}
406
407async fn apply_initial_state_pdus(
408	services: &Services,
409	initial_state: Vec<InitialEvent>,
410	preset: &RoomPreset,
411	sender_user: &UserId,
412	room_id: &RoomId,
413	state_lock: &RoomMutexGuard,
414) -> Result {
415	let is_encrypted = initial_state
416		.iter()
417		.any(|event| event.event_type == StateEventType::RoomEncryption);
418
419	for event in initial_state {
420		services
421			.timeline
422			.build_and_append_pdu(event.into(), sender_user, room_id, state_lock)
423			.boxed()
424			.await?;
425	}
426
427	if !services.config.allow_encryption || is_encrypted {
428		return Ok(());
429	}
430
431	let config = services
432		.config
433		.encryption_enabled_by_default_for_room_type
434		.as_deref();
435
436	let should_encrypt = match config {
437		| Some("all") => true,
438		| Some("invite") =>
439			matches!(preset, RoomPreset::PrivateChat | RoomPreset::TrustedPrivateChat),
440		| _ => false,
441	};
442
443	if !should_encrypt {
444		return Ok(());
445	}
446
447	let algorithm = EventEncryptionAlgorithm::MegolmV1AesSha2;
448	let content = RoomEncryptionEventContent::new(algorithm);
449	services
450		.timeline
451		.build_and_append_pdu(
452			PduBuilder::state(String::new(), &content),
453			sender_user,
454			room_id,
455			state_lock,
456		)
457		.boxed()
458		.await?;
459
460	Ok(())
461}
462
463async fn apply_name_and_topic_pdus(
464	services: &Services,
465	body: &Ruma<create_room::v3::Request>,
466	sender_user: &UserId,
467	room_id: &RoomId,
468	state_lock: &RoomMutexGuard,
469) -> Result {
470	if let Some(name) = &body.name {
471		services
472			.timeline
473			.build_and_append_pdu(
474				PduBuilder::state(String::new(), &RoomNameEventContent::new(name.clone())),
475				sender_user,
476				room_id,
477				state_lock,
478			)
479			.boxed()
480			.await?;
481	}
482
483	if let Some(topic) = &body.topic {
484		services
485			.timeline
486			.build_and_append_pdu(
487				PduBuilder::state(String::new(), &RoomTopicEventContent::new(topic.clone())),
488				sender_user,
489				room_id,
490				state_lock,
491			)
492			.boxed()
493			.await?;
494	}
495
496	Ok(())
497}
498
499async fn process_invites(
500	services: &Services,
501	body: &Ruma<create_room::v3::Request>,
502	sender_user: &UserId,
503	room_id: &RoomId,
504) {
505	// 8. Events implied by invite (and TODO: invite_3pid)
506	body.invite
507		.iter()
508		.stream()
509		.filter(|&user_id| invite_allowed(services, sender_user, user_id))
510		.for_each(async |user_id| {
511			if let Err(e) = services
512				.membership
513				.invite(sender_user, user_id, room_id, None, body.is_direct)
514				.boxed()
515				.await
516			{
517				warn!(%e, "Failed to send invite");
518			}
519		})
520		.await;
521}
522
523/// Gate an invitee against the sender's ignore list, the recipient's ignore
524/// list, and MSC4380 `m.invite_permission_config`.
525async fn invite_allowed(services: &Services, sender_user: &UserId, invitee: &UserId) -> bool {
526	!(services
527		.users
528		.user_is_ignored(sender_user, invitee)
529		.await || services
530		.users
531		.user_is_ignored(invitee, sender_user)
532		.await || services.users.invites_blocked(invitee).await)
533}
534
535async fn finalize_alias_and_directory(
536	services: &Services,
537	body: &Ruma<create_room::v3::Request>,
538	alias: Option<&RoomAliasId>,
539	sender_user: &UserId,
540	room_id: &RoomId,
541) -> Result {
542	if let Some(alias) = alias {
543		services
544			.alias
545			.set_alias_by(alias, room_id, sender_user)?;
546	}
547
548	if body.visibility == room::Visibility::Public {
549		services.directory.set_public(room_id, alias);
550
551		services
552			.admin
553			.notify_loud(&format!("{sender_user} made {room_id} public to the room directory"))
554			.await;
555
556		info!("{sender_user} made {0} public to the room directory", room_id);
557	}
558
559	Ok(())
560}
561
562async fn copy_creator_predecessor_push_rule(
563	services: &Services,
564	creation_content: Option<&Raw<CreationContent>>,
565	sender_user: &UserId,
566	room_id: &RoomId,
567) {
568	let Some(from_room) = creation_content
569		.and_then(|content| {
570			content
571				.get_field::<PreviousRoom>("predecessor")
572				.ok()
573				.flatten()
574		})
575		.map(|predecessor| predecessor.room_id)
576	else {
577		return;
578	};
579
580	services
581		.account_data
582		.copy_room_push_rule(sender_user, &from_room, room_id)
583		.await
584		.inspect_err(|e| warn!(%e, "Failed to copy predecessor push rules"))
585		.ok();
586}
587
588async fn create_create_event(
589	services: &Services,
590	body: &Ruma<create_room::v3::Request>,
591	preset: &RoomPreset,
592	room_version: &RoomVersionId,
593	version_rules: &RoomVersionRules,
594) -> Result<(OwnedRoomId, RoomMutexGuard)> {
595	let _sender_user = body.sender_user();
596
597	let mut create_content = match &body.creation_content {
598		| Some(content) => {
599			let mut content = content
600				.deserialize_as_unchecked::<CanonicalJsonObject>()
601				.map_err(|e| {
602					err!(Request(BadJson(error!(
603						"Failed to deserialise content as canonical JSON: {e}"
604					))))
605				})?;
606
607			if !services.config.federate_created_rooms
608				&& (!services.config.allow_federation || !content.contains_key("m.federate"))
609			{
610				content.insert("m.federate".into(), json!(false).try_into()?);
611			}
612
613			content.insert(
614				"room_version".into(),
615				json!(room_version.as_str())
616					.try_into()
617					.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
618			);
619
620			content
621		},
622		| None => {
623			let content = RoomCreateEventContent::new_v11();
624
625			let mut content =
626				serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
627
628			if !services.config.federate_created_rooms {
629				content.insert("m.federate".into(), json!(false).try_into()?);
630			}
631
632			content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
633			content
634		},
635	};
636
637	if version_rules
638		.authorization
639		.additional_room_creators
640	{
641		let mut additional_creators = body
642			.creation_content
643			.as_ref()
644			.and_then(|c| {
645				c.deserialize_as_unchecked::<CreationContent>()
646					.ok()
647			})
648			.unwrap_or_default()
649			.additional_creators;
650
651		if *preset == RoomPreset::TrustedPrivateChat {
652			additional_creators.extend(body.invite.clone());
653		}
654
655		additional_creators.sort();
656		additional_creators.dedup();
657		if !additional_creators.is_empty() {
658			create_content
659				.insert("additional_creators".into(), json!(additional_creators).try_into()?);
660		}
661	}
662
663	// 1. The room create event, using a placeholder room_id
664	let room_id = ruma::room_id!("!thiswillbereplaced").to_owned();
665	let state_lock = services.state.mutex.lock(&room_id).await;
666	let create_event_id = services
667		.timeline
668		.build_and_append_pdu(
669			PduBuilder {
670				event_type: TimelineEventType::RoomCreate,
671				content: to_raw_value(&create_content)?.into(),
672				state_key: Some(StateKey::new()),
673				..Default::default()
674			},
675			body.sender_user(),
676			&room_id,
677			&state_lock,
678		)
679		.boxed()
680		.await?;
681
682	drop(state_lock);
683
684	// The real room_id is now the event_id.
685	let room_id = OwnedRoomId::from_parts('!', create_event_id.localpart(), None)?;
686	let state_lock = services.state.mutex.lock(&room_id).await;
687
688	Ok((room_id, state_lock))
689}
690
691async fn create_create_event_legacy(
692	services: &Services,
693	body: &Ruma<create_room::v3::Request>,
694	room_version: &RoomVersionId,
695	version_rules: &RoomVersionRules,
696) -> Result<(OwnedRoomId, RoomMutexGuard)> {
697	let room_id: OwnedRoomId = match &body.room_id {
698		| None => RoomId::new_v1(&services.server.name),
699		| Some(custom_id) => custom_room_id_check(services, custom_id).await?,
700	};
701
702	let state_lock = services.state.mutex.lock(&room_id).await;
703
704	let _short_id = services
705		.short
706		.get_or_create_shortroomid(&room_id)
707		.await;
708
709	let create_content = match &body.creation_content {
710		| Some(content) => {
711			let mut content = content
712				.deserialize_as_unchecked::<CanonicalJsonObject>()
713				.map_err(|e| {
714					err!(Request(BadJson(error!(
715						"Failed to deserialise content as canonical JSON: {e}"
716					))))
717				})?;
718
719			if !version_rules.authorization.use_room_create_sender {
720				content.insert(
721					"creator".into(),
722					json!(body.sender_user())
723						.try_into()
724						.map_err(|e| {
725							err!(Request(BadJson(debug_error!("Invalid creation content: {e}"))))
726						})?,
727				);
728			}
729
730			if !services.config.federate_created_rooms
731				&& (!services.config.allow_federation || !content.contains_key("m.federate"))
732			{
733				content.insert("m.federate".into(), json!(false).try_into()?);
734			}
735
736			content.insert(
737				"room_version".into(),
738				json!(room_version.as_str())
739					.try_into()
740					.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
741			);
742
743			content
744		},
745		| None => {
746			let content = if !version_rules.authorization.use_room_create_sender {
747				RoomCreateEventContent::new_v1(body.sender_user().to_owned())
748			} else {
749				RoomCreateEventContent::new_v11()
750			};
751
752			let mut content =
753				serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
754
755			if !services.config.federate_created_rooms {
756				content.insert("m.federate".into(), json!(false).try_into()?);
757			}
758
759			content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
760			content
761		},
762	};
763
764	// 1. The room create event
765	services
766		.timeline
767		.build_and_append_pdu(
768			PduBuilder {
769				event_type: TimelineEventType::RoomCreate,
770				content: to_raw_value(&create_content)?.into(),
771				state_key: Some(StateKey::new()),
772				..Default::default()
773			},
774			body.sender_user(),
775			&room_id,
776			&state_lock,
777		)
778		.boxed()
779		.await?;
780
781	Ok((room_id, state_lock))
782}
783
784/// creates the power_levels_content for the PDU builder
785fn default_power_levels_content(
786	version_rules: &RoomVersionRules,
787	default_power_level_content_override: Option<&JsonValue>,
788	power_level_content_override: Option<&Raw<RoomPowerLevelsContentOverride>>,
789	preset: &RoomPreset,
790	users: BTreeMap<OwnedUserId, Int>,
791) -> Result<JsonValue> {
792	use serde_json::to_value;
793
794	let mut power_levels_content = RoomPowerLevelsEventContent::new(&version_rules.authorization);
795	power_levels_content.users = users;
796
797	let mut power_levels_content = to_value(power_levels_content)?;
798
799	// secure proper defaults of sensitive/dangerous permissions that moderators
800	// (power level 50) should not have easy access to
801	power_levels_content["events"]["m.room.power_levels"] = json!(100);
802	power_levels_content["events"]["m.room.server_acl"] = json!(100);
803	power_levels_content["events"]["m.room.encryption"] = json!(100);
804	power_levels_content["events"]["m.room.history_visibility"] = json!(100);
805
806	if version_rules
807		.authorization
808		.explicitly_privilege_room_creators
809	{
810		power_levels_content["events"]["m.room.tombstone"] = json!(150);
811	} else {
812		power_levels_content["events"]["m.room.tombstone"] = json!(100);
813	}
814
815	// always allow users to respond (not post new) to polls. this is primarily
816	// useful in read-only announcement rooms that post a public poll.
817	power_levels_content["events"]["org.matrix.msc3381.poll.response"] = json!(0);
818	power_levels_content["events"]["m.poll.response"] = json!(0);
819
820	// public_chat: pin invite and call-setup events at PL 50. Synapse pins
821	// invite and m.call.invite here; the MSC3401 entries are tuwunel-only.
822	if *preset == RoomPreset::PublicChat {
823		power_levels_content["invite"] = json!(50);
824		power_levels_content["events"]["m.call.invite"] = json!(50);
825		power_levels_content["events"]["m.call"] = json!(50);
826		power_levels_content["events"]["m.call.member"] = json!(50);
827		power_levels_content["events"]["org.matrix.msc3401.call"] = json!(50);
828		power_levels_content["events"]["org.matrix.msc3401.call.member"] = json!(50);
829	}
830
831	if let Some(default_power_level_content_override) = default_power_level_content_override {
832		let overrides = default_power_level_content_override
833			.as_object()
834			.expect("default_power_level_content_override is validated at startup")
835			.iter()
836			.map(|(key, value)| (key.clone(), value.clone()));
837
838		merge_power_level_content_override(&mut power_levels_content, overrides);
839	}
840
841	if let Some(power_level_content_override) = power_level_content_override {
842		let overrides: JsonObject =
843			serde_json::from_str(power_level_content_override.json().get()).map_err(|e| {
844				err!(Request(BadJson("Invalid power_level_content_override: {e:?}")))
845			})?;
846
847		merge_power_level_content_override(&mut power_levels_content, overrides);
848	}
849
850	Ok(power_levels_content)
851}
852
853/// Replace each top-level power-levels key wholesale; no deep merge.
854fn merge_power_level_content_override(
855	power_levels_content: &mut JsonValue,
856	overrides: impl IntoIterator<Item = (String, JsonValue)>,
857) {
858	power_levels_content
859		.as_object_mut()
860		.expect("power levels content must serialize to an object")
861		.extend(overrides);
862}
863
864/// if a room is being created with a room alias, run our checks
865async fn room_alias_check(
866	services: &Services,
867	room_alias_name: &str,
868	appservice_info: Option<&RegistrationInfo>,
869) -> Result<OwnedRoomAliasId> {
870	// Basic checks on the room alias validity
871	if room_alias_name.contains(':') {
872		return Err!(Request(InvalidParam(
873			"Room alias contained `:` which is not allowed. Please note that this expects a \
874			 localpart, not the full room alias.",
875		)));
876	} else if room_alias_name.contains(char::is_whitespace) {
877		return Err!(Request(InvalidParam(
878			"Room alias contained spaces which is not a valid room alias.",
879		)));
880	}
881
882	// check if room alias is forbidden
883	if services
884		.config
885		.forbidden_alias_names
886		.is_match(room_alias_name)
887	{
888		return Err!(Request(Unknown("Room alias name is forbidden.")));
889	}
890
891	let server_name = services.globals.server_name();
892	let full_room_alias = OwnedRoomAliasId::parse(format!("#{room_alias_name}:{server_name}"))
893		.map_err(|e| {
894			err!(Request(InvalidParam(debug_error!(
895				?e,
896				?room_alias_name,
897				"Failed to parse room alias.",
898			))))
899		})?;
900
901	if services
902		.alias
903		.resolve_local_alias(&full_room_alias)
904		.await
905		.is_ok()
906	{
907		return Err!(Request(RoomInUse("Room alias already exists.")));
908	}
909
910	if let Some(info) = appservice_info {
911		if !info.aliases.is_match(full_room_alias.as_str()) {
912			return Err!(Request(Exclusive("Room alias is not in namespace.")));
913		}
914	} else if services
915		.appservice
916		.is_exclusive_alias(&full_room_alias)
917		.await
918	{
919		return Err!(Request(Exclusive("Room alias reserved by appservice.",)));
920	}
921
922	debug_info!("Full room alias: {full_room_alias}");
923
924	Ok(full_room_alias)
925}
926
927/// if a room is being created with a custom room ID, run our checks against it
928async fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
929	// apply forbidden room alias checks to custom room IDs too
930	if services
931		.config
932		.forbidden_alias_names
933		.is_match(custom_room_id)
934	{
935		return Err!(Request(Unknown("Custom room ID is forbidden.")));
936	}
937
938	if custom_room_id.contains(':') {
939		return Err!(Request(InvalidParam(
940			"Custom room ID contained `:` which is not allowed. Please note that this expects a \
941			 localpart, not the full room ID.",
942		)));
943	} else if custom_room_id.contains(char::is_whitespace) {
944		return Err!(Request(InvalidParam(
945			"Custom room ID contained spaces which is not valid."
946		)));
947	}
948
949	let server_name = services.globals.server_name();
950	let full_room_id = format!("!{custom_room_id}:{server_name}");
951
952	let room_id = OwnedRoomId::parse(full_room_id)
953		.inspect(|full_room_id| debug_info!(?full_room_id, "Full custom room ID"))
954		.inspect_err(|e| {
955			warn!(?e, ?custom_room_id, "Failed to create room with custom room ID");
956		})?;
957
958	// check if room ID doesn't already exist instead of erroring on auth check
959	if services
960		.short
961		.get_shortroomid(&room_id)
962		.await
963		.is_ok()
964	{
965		return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
966	}
967
968	Ok(room_id)
969}
970
971async fn can_publish_directory_check(
972	services: &Services,
973	body: &Ruma<create_room::v3::Request>,
974) -> Result {
975	if !services
976		.server
977		.config
978		.lockdown_public_room_directory
979		|| body.appservice_info.is_some()
980		|| body.visibility != room::Visibility::Public
981		|| services
982			.admin
983			.user_is_admin(body.sender_user())
984			.await
985	{
986		return Ok(());
987	}
988
989	let msg = format!(
990		"Non-admin user {} tried to publish new to the directory while \
991		 lockdown_public_room_directory is enabled",
992		body.sender_user(),
993	);
994
995	warn!("{msg}");
996	services.admin.notify(&msg).await;
997
998	Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")))
999}
1000
1001async fn can_create_room_check(
1002	services: &Services,
1003	body: &Ruma<create_room::v3::Request>,
1004) -> Result {
1005	if !services.config.allow_room_creation
1006		&& body.appservice_info.is_none()
1007		&& !services
1008			.admin
1009			.user_is_admin(body.sender_user())
1010			.await
1011	{
1012		return Err!(Request(Forbidden("Room creation has been disabled.",)));
1013	}
1014
1015	Ok(())
1016}
1017
1018#[derive(Deserialize)]
1019struct InitialEvent {
1020	#[serde(rename = "type")]
1021	event_type: StateEventType,
1022
1023	#[serde(default = "StateKey::new")]
1024	state_key: StateKey,
1025
1026	content: Content,
1027}
1028
1029impl From<InitialEvent> for PduBuilder {
1030	fn from(value: InitialEvent) -> Self {
1031		Self {
1032			event_type: value.event_type.into(),
1033			content: value.content,
1034			unsigned: None,
1035			state_key: Some(value.state_key),
1036			redacts: None,
1037			timestamp: None,
1038		}
1039	}
1040}
1041
1042fn take_initial(
1043	initial_state: &mut Vec<InitialEvent>,
1044	event_type: &StateEventType,
1045	state_key: &str,
1046) -> Option<InitialEvent> {
1047	initial_state
1048		.extract_if(.., |event| &event.event_type == event_type && event.state_key == state_key)
1049		.next()
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054	use tuwunel_core::matrix::room_version::rules;
1055
1056	use super::*;
1057
1058	fn guest_access(pdu: &PduBuilder) -> GuestAccess {
1059		pdu.content
1060			.deserialize_as_unchecked::<RoomGuestAccessEventContent>()
1061			.expect("guest access content")
1062			.guest_access
1063	}
1064
1065	#[test]
1066	fn default_power_levels_content_applies_server_default_override() {
1067		let version_rules = rules(&RoomVersionId::V11).expect("supported room version");
1068
1069		let content = default_power_levels_content(
1070			&version_rules,
1071			Some(&json!({ "users_default": 50 })),
1072			None,
1073			&RoomPreset::PrivateChat,
1074			BTreeMap::new(),
1075		)
1076		.expect("power levels content");
1077
1078		assert_eq!(content["users_default"], json!(50));
1079	}
1080
1081	#[test]
1082	fn request_override_wins_over_server_default_override() {
1083		let version_rules = rules(&RoomVersionId::V11).expect("supported room version");
1084		let request_override =
1085			Raw::from_json(to_raw_value(&json!({ "users_default": 75 })).expect("raw json"));
1086
1087		let content = default_power_levels_content(
1088			&version_rules,
1089			Some(&json!({ "users_default": 50 })),
1090			Some(&request_override),
1091			&RoomPreset::PrivateChat,
1092			BTreeMap::new(),
1093		)
1094		.expect("power levels content");
1095
1096		assert_eq!(content["users_default"], json!(75));
1097	}
1098
1099	#[test]
1100	fn default_override_preserves_explicit_user_power_levels() {
1101		let version_rules = rules(&RoomVersionId::V11).expect("supported room version");
1102		let creator = OwnedUserId::try_from("@alice:example.com").expect("valid user id");
1103		let users = BTreeMap::from([(creator.clone(), int!(100))]);
1104
1105		let content = default_power_levels_content(
1106			&version_rules,
1107			Some(&json!({ "users_default": 50 })),
1108			None,
1109			&RoomPreset::PrivateChat,
1110			users,
1111		)
1112		.expect("power levels content");
1113
1114		assert_eq!(content["users_default"], json!(50));
1115		assert_eq!(content["users"][creator.as_str()], json!(100));
1116	}
1117
1118	#[test]
1119	fn public_chat_omits_default_guest_access() {
1120		assert!(guest_access_pdu(None, &RoomPreset::PublicChat).is_none());
1121	}
1122
1123	#[test]
1124	fn private_presets_default_to_guest_access() {
1125		for preset in [RoomPreset::PrivateChat, RoomPreset::TrustedPrivateChat] {
1126			let pdu = guest_access_pdu(None, &preset).expect("guest access pdu");
1127
1128			assert_eq!(pdu.event_type, TimelineEventType::RoomGuestAccess);
1129			assert_eq!(pdu.state_key.as_deref(), Some(""));
1130			assert_eq!(guest_access(&pdu), GuestAccess::CanJoin);
1131		}
1132	}
1133
1134	#[test]
1135	fn explicit_guest_access_survives_public_preset() {
1136		let explicit = PduBuilder::state(
1137			String::new(),
1138			&RoomGuestAccessEventContent::new(GuestAccess::Forbidden),
1139		);
1140
1141		let pdu = guest_access_pdu(Some(explicit), &RoomPreset::PublicChat)
1142			.expect("explicit guest access pdu");
1143
1144		assert_eq!(guest_access(&pdu), GuestAccess::Forbidden);
1145	}
1146}