Skip to main content

tuwunel_api/client/room/
create.rs

1use std::collections::BTreeMap;
2
3use axum::extract::State;
4use futures::FutureExt;
5use itertools::Itertools;
6use ruma::{
7	CanonicalJsonObject, EventEncryptionAlgorithm, Int, OwnedRoomAliasId, OwnedRoomId,
8	OwnedUserId, RoomId, RoomVersionId,
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::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::{
37	json,
38	value::{RawValue, to_raw_value},
39};
40use tuwunel_core::{
41	Err, Result, debug_info, debug_warn, err, info,
42	matrix::{StateKey, pdu::PduBuilder, room_version},
43	utils::{BoolExt, option::OptionExt},
44	warn,
45};
46use tuwunel_service::{Services, appservice::RegistrationInfo, rooms::state::RoomMutexGuard};
47
48use crate::{Ruma, client::utils::invite_check};
49
50/// # `POST /_matrix/client/v3/createRoom`
51///
52/// Creates a new room.
53///
54/// - Room ID is randomly generated
55/// - Create alias if `room_alias_name` is set
56/// - Send create event
57/// - Join sender user
58/// - Send power levels event
59/// - Send canonical room alias
60/// - Send join rules
61/// - Send history visibility
62/// - Send guest access
63/// - Send events listed in initial state
64/// - Send events implied by `name` and `topic`
65/// - Send invite events
66pub(crate) async fn create_room_route(
67	State(services): State<crate::State>,
68	body: Ruma<create_room::v3::Request>,
69) -> Result<create_room::v3::Response> {
70	can_create_room_check(&services, &body).await?;
71	can_publish_directory_check(&services, &body).await?;
72
73	// Figure out preset. We need it for preset specific events
74	let preset = body
75		.preset
76		.clone()
77		.unwrap_or(match &body.visibility {
78			| room::Visibility::Public => RoomPreset::PublicChat,
79			| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
80		});
81
82	// Determine room version
83	let (room_version, version_rules) = body
84		.room_version
85		.as_ref()
86		.map_or(Ok(&services.server.config.default_room_version), |version| {
87			services
88				.config
89				.supported_room_version(version)
90				.then_ok_or_else(version, || {
91					err!(Request(UnsupportedRoomVersion(
92						"This server does not support room version {version:?}"
93					)))
94				})
95		})
96		.and_then(|version| Ok((version, room_version::rules(version)?)))?;
97
98	// Error on existing alias before committing to creation.
99	let alias = body
100		.room_alias_name
101		.as_ref()
102		.map_async(|alias| room_alias_check(&services, alias, body.appservice_info.as_ref()))
103		.await
104		.transpose()?;
105
106	// Increment and hold the counter; the room will sync atomically to clients
107	// which is preferable.
108	let next_count = services.globals.next_count();
109
110	// 1. Create the create event.
111	let (room_id, state_lock) = match version_rules.room_id_format {
112		| RoomIdFormatVersion::V1 =>
113			create_create_event_legacy(&services, &body, room_version, &version_rules).await?,
114		| RoomIdFormatVersion::V2 =>
115			create_create_event(&services, &body, &preset, room_version, &version_rules)
116				.await
117				.map_err(|e| {
118					err!(Request(InvalidParam("Error while creating m.room.create event: {e}")))
119				})?,
120	};
121
122	// 2. Let the room creator join
123	let sender_user = body.sender_user();
124	services
125		.timeline
126		.build_and_append_pdu(
127			PduBuilder::state(sender_user.to_string(), &RoomMemberEventContent {
128				displayname: services.users.displayname(sender_user).await.ok(),
129				avatar_url: services.users.avatar_url(sender_user).await.ok(),
130				blurhash: services.users.blurhash(sender_user).await.ok(),
131				is_direct: Some(body.is_direct),
132				..RoomMemberEventContent::new(MembershipState::Join)
133			}),
134			sender_user,
135			&room_id,
136			&state_lock,
137		)
138		.boxed()
139		.await?;
140
141	// 3. Power levels
142	let mut users = if !version_rules
143		.authorization
144		.explicitly_privilege_room_creators
145	{
146		BTreeMap::from_iter([(sender_user.to_owned(), int!(100))])
147	} else {
148		BTreeMap::new()
149	};
150
151	if preset == RoomPreset::TrustedPrivateChat {
152		for invite in &body.invite {
153			if services
154				.users
155				.user_is_ignored(sender_user, invite)
156				.await
157			{
158				continue;
159			} else if services
160				.users
161				.user_is_ignored(invite, sender_user)
162				.await
163			{
164				// silently drop the invite to the recipient if they've been ignored by the
165				// sender, pretend it worked
166				continue;
167			}
168
169			if !version_rules
170				.authorization
171				.additional_room_creators
172			{
173				users.insert(invite.clone(), int!(100));
174			}
175		}
176	}
177
178	let power_levels_content = default_power_levels_content(
179		&version_rules,
180		body.power_level_content_override.as_ref(),
181		&preset,
182		users,
183	)?;
184
185	services
186		.timeline
187		.build_and_append_pdu(
188			PduBuilder {
189				event_type: TimelineEventType::RoomPowerLevels,
190				content: to_raw_value(&power_levels_content)?,
191				state_key: Some(StateKey::new()),
192				..Default::default()
193			},
194			sender_user,
195			&room_id,
196			&state_lock,
197		)
198		.boxed()
199		.await?;
200
201	// 4. Canonical room alias
202	if let Some(room_alias_id) = &alias {
203		services
204			.timeline
205			.build_and_append_pdu(
206				PduBuilder::state(String::new(), &RoomCanonicalAliasEventContent {
207					alias: Some(room_alias_id.to_owned()),
208					alt_aliases: vec![],
209				}),
210				sender_user,
211				&room_id,
212				&state_lock,
213			)
214			.boxed()
215			.await?;
216	}
217
218	// 5. Events set by preset
219	let mut initial_state = body
220		.initial_state
221		.iter()
222		.map(|state| Ok(state.deserialize_as_unchecked::<InitialEvent>()?))
223		.filter_ok(|event| {
224			services.config.allow_encryption || event.event_type != StateEventType::RoomEncryption
225		})
226		.filter_ok(|event| {
227			// client/appservice workaround: if a user sends an initial_state event with a
228			// state event in there with the content of literally `{}` (not null or empty
229			// string), let's just skip it over and warn.
230			if event.content.get() == "{}" {
231				debug_warn!("skipping empty initial state event of type {}", event.event_type);
232				false
233			} else {
234				true
235			}
236		})
237		.filter_ok(|event| body.name.is_none() || event.event_type != StateEventType::RoomName)
238		.filter_ok(|event| body.topic.is_none() || event.event_type != StateEventType::RoomTopic)
239		.collect::<Result<Vec<_>>>()?;
240
241	let join_rule_pdubuilder =
242		take_initial(&mut initial_state, &StateEventType::RoomJoinRules, "")
243			.map(Into::into)
244			.unwrap_or_else(|| {
245				PduBuilder::state(
246					String::new(),
247					&RoomJoinRulesEventContent::new(match preset {
248						| RoomPreset::PublicChat => JoinRule::Public,
249						// according to spec "invite" is the default
250						| _ => JoinRule::Invite,
251					}),
252				)
253			});
254
255	let history_visibility_pdubuilder =
256		take_initial(&mut initial_state, &StateEventType::RoomHistoryVisibility, "")
257			.map(Into::into)
258			.unwrap_or_else(|| {
259				PduBuilder::state(
260					String::new(),
261					&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared),
262				)
263			});
264
265	let guest_access_pdubuilder =
266		take_initial(&mut initial_state, &StateEventType::RoomGuestAccess, "")
267			.map(Into::into)
268			.unwrap_or_else(|| {
269				PduBuilder::state(
270					String::new(),
271					&RoomGuestAccessEventContent::new(match preset {
272						| RoomPreset::PublicChat => GuestAccess::Forbidden,
273						| _ => GuestAccess::CanJoin,
274					}),
275				)
276			});
277
278	// 5.1 Join Rules
279	services
280		.timeline
281		.build_and_append_pdu(join_rule_pdubuilder, sender_user, &room_id, &state_lock)
282		.boxed()
283		.await?;
284
285	// 5.2 History Visibility
286	services
287		.timeline
288		.build_and_append_pdu(history_visibility_pdubuilder, sender_user, &room_id, &state_lock)
289		.boxed()
290		.await?;
291
292	// 5.3 Guest Access
293	services
294		.timeline
295		.build_and_append_pdu(guest_access_pdubuilder, sender_user, &room_id, &state_lock)
296		.boxed()
297		.await?;
298
299	// 6. Events listed in initial_state
300	let is_encrypted = initial_state
301		.iter()
302		.any(|event| event.event_type == StateEventType::RoomEncryption);
303
304	for event in initial_state {
305		services
306			.timeline
307			.build_and_append_pdu(event.into(), sender_user, &room_id, &state_lock)
308			.boxed()
309			.await?;
310	}
311
312	if services.config.allow_encryption && !is_encrypted {
313		use RoomPreset::*;
314
315		let config = services
316			.config
317			.encryption_enabled_by_default_for_room_type
318			.as_deref();
319
320		let should_encrypt = match config {
321			| Some("all") => true,
322			| Some("invite") => matches!(preset, PrivateChat | TrustedPrivateChat),
323			| _ => false,
324		};
325
326		if should_encrypt {
327			let algorithm = EventEncryptionAlgorithm::MegolmV1AesSha2;
328			let content = RoomEncryptionEventContent::new(algorithm);
329			services
330				.timeline
331				.build_and_append_pdu(
332					PduBuilder::state(String::new(), &content),
333					sender_user,
334					&room_id,
335					&state_lock,
336				)
337				.boxed()
338				.await?;
339		}
340	}
341
342	// 7. Events implied by name and topic
343	if let Some(name) = &body.name {
344		services
345			.timeline
346			.build_and_append_pdu(
347				PduBuilder::state(String::new(), &RoomNameEventContent::new(name.clone())),
348				sender_user,
349				&room_id,
350				&state_lock,
351			)
352			.boxed()
353			.await?;
354	}
355
356	if let Some(topic) = &body.topic {
357		services
358			.timeline
359			.build_and_append_pdu(
360				PduBuilder::state(String::new(), &RoomTopicEventContent::new(topic.clone())),
361				sender_user,
362				&room_id,
363				&state_lock,
364			)
365			.boxed()
366			.await?;
367	}
368
369	drop(next_count);
370	drop(state_lock);
371
372	// if inviting anyone with room creation and invite check passes
373	if (!body.invite.is_empty() || !body.invite_3pid.is_empty())
374		&& invite_check(&services, sender_user, &room_id)
375			.await
376			.is_ok()
377	{
378		// 8. Events implied by invite (and TODO: invite_3pid)
379		for user_id in &body.invite {
380			if services
381				.users
382				.user_is_ignored(sender_user, user_id)
383				.await
384			{
385				continue;
386			} else if services
387				.users
388				.user_is_ignored(user_id, sender_user)
389				.await
390			{
391				// silently drop the invite to the recipient if they've been ignored by the
392				// sender, pretend it worked
393				continue;
394			}
395
396			if let Err(e) = services
397				.membership
398				.invite(sender_user, user_id, &room_id, None, body.is_direct)
399				.boxed()
400				.await
401			{
402				warn!(%e, "Failed to send invite");
403			}
404		}
405	}
406
407	// Homeserver specific stuff
408	if let Some(alias) = alias {
409		services
410			.alias
411			.set_alias_by(&alias, &room_id, sender_user)?;
412	}
413
414	if body.visibility == room::Visibility::Public {
415		services.directory.set_public(&room_id);
416
417		if services.server.config.admin_room_notices {
418			services
419				.admin
420				.send_text(&format!("{sender_user} made {room_id} public to the room directory"))
421				.await;
422		}
423		info!("{sender_user} made {0} public to the room directory", &room_id);
424	}
425
426	info!("{sender_user} created a room with room ID {room_id}");
427
428	Ok(create_room::v3::Response::new(room_id))
429}
430
431async fn create_create_event(
432	services: &Services,
433	body: &Ruma<create_room::v3::Request>,
434	preset: &RoomPreset,
435	room_version: &RoomVersionId,
436	version_rules: &RoomVersionRules,
437) -> Result<(OwnedRoomId, RoomMutexGuard)> {
438	let _sender_user = body.sender_user();
439
440	let mut create_content = match &body.creation_content {
441		| Some(content) => {
442			let mut content = content
443				.deserialize_as_unchecked::<CanonicalJsonObject>()
444				.map_err(|e| {
445					err!(Request(BadJson(error!(
446						"Failed to deserialise content as canonical JSON: {e}"
447					))))
448				})?;
449
450			if !services.config.federate_created_rooms
451				&& (!services.config.allow_federation || !content.contains_key("m.federate"))
452			{
453				content.insert("m.federate".into(), json!(false).try_into()?);
454			}
455
456			content.insert(
457				"room_version".into(),
458				json!(room_version.as_str())
459					.try_into()
460					.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
461			);
462
463			content
464		},
465		| None => {
466			let content = RoomCreateEventContent::new_v11();
467
468			let mut content =
469				serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
470
471			if !services.config.federate_created_rooms {
472				content.insert("m.federate".into(), json!(false).try_into()?);
473			}
474
475			content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
476			content
477		},
478	};
479
480	if version_rules
481		.authorization
482		.additional_room_creators
483	{
484		let mut additional_creators = body
485			.creation_content
486			.as_ref()
487			.and_then(|c| {
488				c.deserialize_as_unchecked::<CreationContent>()
489					.ok()
490			})
491			.unwrap_or_default()
492			.additional_creators;
493
494		if *preset == RoomPreset::TrustedPrivateChat {
495			additional_creators.extend(body.invite.clone());
496		}
497
498		additional_creators.sort();
499		additional_creators.dedup();
500		if !additional_creators.is_empty() {
501			create_content
502				.insert("additional_creators".into(), json!(additional_creators).try_into()?);
503		}
504	}
505
506	// 1. The room create event, using a placeholder room_id
507	let room_id = ruma::room_id!("!thiswillbereplaced").to_owned();
508	let state_lock = services.state.mutex.lock(&room_id).await;
509	let create_event_id = services
510		.timeline
511		.build_and_append_pdu(
512			PduBuilder {
513				event_type: TimelineEventType::RoomCreate,
514				content: to_raw_value(&create_content)?,
515				state_key: Some(StateKey::new()),
516				..Default::default()
517			},
518			body.sender_user(),
519			&room_id,
520			&state_lock,
521		)
522		.boxed()
523		.await?;
524
525	drop(state_lock);
526
527	// The real room_id is now the event_id.
528	let room_id = OwnedRoomId::from_parts('!', create_event_id.localpart(), None)?;
529	let state_lock = services.state.mutex.lock(&room_id).await;
530
531	Ok((room_id, state_lock))
532}
533
534async fn create_create_event_legacy(
535	services: &Services,
536	body: &Ruma<create_room::v3::Request>,
537	room_version: &RoomVersionId,
538	_version_rules: &RoomVersionRules,
539) -> Result<(OwnedRoomId, RoomMutexGuard)> {
540	let room_id: OwnedRoomId = match &body.room_id {
541		| None => RoomId::new_v1(&services.server.name),
542		| Some(custom_id) => custom_room_id_check(services, custom_id).await?,
543	};
544
545	let state_lock = services.state.mutex.lock(&room_id).await;
546
547	let _short_id = services
548		.short
549		.get_or_create_shortroomid(&room_id)
550		.await;
551
552	let create_content = match &body.creation_content {
553		| Some(content) => {
554			use RoomVersionId::*;
555
556			let mut content = content
557				.deserialize_as_unchecked::<CanonicalJsonObject>()
558				.map_err(|e| {
559					err!(Request(BadJson(error!(
560						"Failed to deserialise content as canonical JSON: {e}"
561					))))
562				})?;
563
564			match room_version {
565				| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
566					content.insert(
567						"creator".into(),
568						json!(body.sender_user())
569							.try_into()
570							.map_err(|e| {
571								err!(Request(BadJson(debug_error!(
572									"Invalid creation content: {e}"
573								))))
574							})?,
575					);
576				},
577				| _ => {
578					// V11+ removed the "creator" key
579				},
580			}
581
582			if !services.config.federate_created_rooms
583				&& (!services.config.allow_federation || !content.contains_key("m.federate"))
584			{
585				content.insert("m.federate".into(), json!(false).try_into()?);
586			}
587
588			content.insert(
589				"room_version".into(),
590				json!(room_version.as_str())
591					.try_into()
592					.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
593			);
594
595			content
596		},
597		| None => {
598			use RoomVersionId::*;
599
600			let content = match room_version {
601				| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
602					RoomCreateEventContent::new_v1(body.sender_user().to_owned()),
603				| _ => RoomCreateEventContent::new_v11(),
604			};
605
606			let mut content =
607				serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
608
609			if !services.config.federate_created_rooms {
610				content.insert("m.federate".into(), json!(false).try_into()?);
611			}
612
613			content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
614			content
615		},
616	};
617
618	// 1. The room create event
619	services
620		.timeline
621		.build_and_append_pdu(
622			PduBuilder {
623				event_type: TimelineEventType::RoomCreate,
624				content: to_raw_value(&create_content)?,
625				state_key: Some(StateKey::new()),
626				..Default::default()
627			},
628			body.sender_user(),
629			&room_id,
630			&state_lock,
631		)
632		.boxed()
633		.await?;
634
635	Ok((room_id, state_lock))
636}
637
638/// creates the power_levels_content for the PDU builder
639fn default_power_levels_content(
640	version_rules: &RoomVersionRules,
641	power_level_content_override: Option<&Raw<RoomPowerLevelsContentOverride>>,
642	preset: &RoomPreset,
643	users: BTreeMap<OwnedUserId, Int>,
644) -> Result<serde_json::Value> {
645	use serde_json::to_value;
646
647	let mut power_levels_content = RoomPowerLevelsEventContent::new(&version_rules.authorization);
648	power_levels_content.users = users;
649
650	let mut power_levels_content = to_value(power_levels_content)?;
651
652	// secure proper defaults of sensitive/dangerous permissions that moderators
653	// (power level 50) should not have easy access to
654	power_levels_content["events"]["m.room.power_levels"] = json!(100);
655	power_levels_content["events"]["m.room.server_acl"] = json!(100);
656	power_levels_content["events"]["m.room.encryption"] = json!(100);
657	power_levels_content["events"]["m.room.history_visibility"] = json!(100);
658
659	if version_rules
660		.authorization
661		.explicitly_privilege_room_creators
662	{
663		power_levels_content["events"]["m.room.tombstone"] = json!(150);
664	} else {
665		power_levels_content["events"]["m.room.tombstone"] = json!(100);
666	}
667
668	// always allow users to respond (not post new) to polls. this is primarily
669	// useful in read-only announcement rooms that post a public poll.
670	power_levels_content["events"]["org.matrix.msc3381.poll.response"] = json!(0);
671	power_levels_content["events"]["m.poll.response"] = json!(0);
672
673	// public_chat: pin invite and call-setup events at PL 50. Synapse pins
674	// invite and m.call.invite here; the MSC3401 entries are tuwunel-only.
675	if *preset == RoomPreset::PublicChat {
676		power_levels_content["invite"] = json!(50);
677		power_levels_content["events"]["m.call.invite"] = json!(50);
678		power_levels_content["events"]["m.call"] = json!(50);
679		power_levels_content["events"]["m.call.member"] = json!(50);
680		power_levels_content["events"]["org.matrix.msc3401.call"] = json!(50);
681		power_levels_content["events"]["org.matrix.msc3401.call.member"] = json!(50);
682	}
683
684	if let Some(power_level_content_override) = power_level_content_override {
685		let json: JsonObject = serde_json::from_str(power_level_content_override.json().get())
686			.map_err(|e| err!(Request(BadJson("Invalid power_level_content_override: {e:?}"))))?;
687
688		for (key, value) in json {
689			power_levels_content[key] = value;
690		}
691	}
692
693	Ok(power_levels_content)
694}
695
696/// if a room is being created with a room alias, run our checks
697async fn room_alias_check(
698	services: &Services,
699	room_alias_name: &str,
700	appservice_info: Option<&RegistrationInfo>,
701) -> Result<OwnedRoomAliasId> {
702	// Basic checks on the room alias validity
703	if room_alias_name.contains(':') {
704		return Err!(Request(InvalidParam(
705			"Room alias contained `:` which is not allowed. Please note that this expects a \
706			 localpart, not the full room alias.",
707		)));
708	} else if room_alias_name.contains(char::is_whitespace) {
709		return Err!(Request(InvalidParam(
710			"Room alias contained spaces which is not a valid room alias.",
711		)));
712	}
713
714	// check if room alias is forbidden
715	if services
716		.config
717		.forbidden_alias_names
718		.is_match(room_alias_name)
719	{
720		return Err!(Request(Unknown("Room alias name is forbidden.")));
721	}
722
723	let server_name = services.globals.server_name();
724	let full_room_alias = OwnedRoomAliasId::parse(format!("#{room_alias_name}:{server_name}"))
725		.map_err(|e| {
726			err!(Request(InvalidParam(debug_error!(
727				?e,
728				?room_alias_name,
729				"Failed to parse room alias.",
730			))))
731		})?;
732
733	if services
734		.alias
735		.resolve_local_alias(&full_room_alias)
736		.await
737		.is_ok()
738	{
739		return Err!(Request(RoomInUse("Room alias already exists.")));
740	}
741
742	if let Some(info) = appservice_info {
743		if !info.aliases.is_match(full_room_alias.as_str()) {
744			return Err!(Request(Exclusive("Room alias is not in namespace.")));
745		}
746	} else if services
747		.appservice
748		.is_exclusive_alias(&full_room_alias)
749		.await
750	{
751		return Err!(Request(Exclusive("Room alias reserved by appservice.",)));
752	}
753
754	debug_info!("Full room alias: {full_room_alias}");
755
756	Ok(full_room_alias)
757}
758
759/// if a room is being created with a custom room ID, run our checks against it
760async fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
761	// apply forbidden room alias checks to custom room IDs too
762	if services
763		.config
764		.forbidden_alias_names
765		.is_match(custom_room_id)
766	{
767		return Err!(Request(Unknown("Custom room ID is forbidden.")));
768	}
769
770	if custom_room_id.contains(':') {
771		return Err!(Request(InvalidParam(
772			"Custom room ID contained `:` which is not allowed. Please note that this expects a \
773			 localpart, not the full room ID.",
774		)));
775	} else if custom_room_id.contains(char::is_whitespace) {
776		return Err!(Request(InvalidParam(
777			"Custom room ID contained spaces which is not valid."
778		)));
779	}
780
781	let server_name = services.globals.server_name();
782	let full_room_id = format!("!{custom_room_id}:{server_name}");
783
784	let room_id = OwnedRoomId::parse(full_room_id)
785		.inspect(|full_room_id| debug_info!(?full_room_id, "Full custom room ID"))
786		.inspect_err(|e| {
787			warn!(?e, ?custom_room_id, "Failed to create room with custom room ID");
788		})?;
789
790	// check if room ID doesn't already exist instead of erroring on auth check
791	if services
792		.short
793		.get_shortroomid(&room_id)
794		.await
795		.is_ok()
796	{
797		return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
798	}
799
800	Ok(room_id)
801}
802
803async fn can_publish_directory_check(
804	services: &Services,
805	body: &Ruma<create_room::v3::Request>,
806) -> Result {
807	if !services
808		.server
809		.config
810		.lockdown_public_room_directory
811		|| body.appservice_info.is_some()
812		|| body.visibility != room::Visibility::Public
813		|| services
814			.admin
815			.user_is_admin(body.sender_user())
816			.await
817	{
818		return Ok(());
819	}
820
821	let msg = format!(
822		"Non-admin user {} tried to publish new to the directory while \
823		 lockdown_public_room_directory is enabled",
824		body.sender_user(),
825	);
826
827	warn!("{msg}");
828	if services.server.config.admin_room_notices {
829		services.admin.notice(&msg).await;
830	}
831
832	Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")))
833}
834
835async fn can_create_room_check(
836	services: &Services,
837	body: &Ruma<create_room::v3::Request>,
838) -> Result {
839	if !services.config.allow_room_creation
840		&& body.appservice_info.is_none()
841		&& !services
842			.admin
843			.user_is_admin(body.sender_user())
844			.await
845	{
846		return Err!(Request(Forbidden("Room creation has been disabled.",)));
847	}
848
849	Ok(())
850}
851
852#[derive(Deserialize)]
853struct InitialEvent {
854	#[serde(rename = "type")]
855	event_type: StateEventType,
856
857	#[serde(default = "StateKey::new")]
858	state_key: StateKey,
859
860	content: Box<RawValue>,
861}
862
863impl From<InitialEvent> for PduBuilder {
864	fn from(value: InitialEvent) -> Self {
865		Self {
866			event_type: value.event_type.into(),
867			content: value.content,
868			unsigned: None,
869			state_key: Some(value.state_key),
870			redacts: None,
871			timestamp: None,
872		}
873	}
874}
875
876fn take_initial(
877	initial_state: &mut Vec<InitialEvent>,
878	event_type: &StateEventType,
879	state_key: &str,
880) -> Option<InitialEvent> {
881	initial_state
882		.extract_if(.., |event| &event.event_type == event_type && event.state_key == state_key)
883		.next()
884}