use std::{ collections::{BTreeMap, BTreeSet, HashSet}, sync::atomic::{AtomicU64, Ordering::SeqCst}, }; use axum::extract::State; use conduwuit::{ Err, Error, Result, debug, debug_info, err, info, matrix::{StateKey, pdu::PartialPdu}, trace, utils::{self, to_canonical_object}, warn, }; use conduwuit_service::{Services, appservice::RegistrationInfo}; use futures::FutureExt; use ruma::{ CanonicalJsonObject, CanonicalJsonValue, Int, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedUserId, RoomAliasId, RoomId, UInt, api::{ client::room::{ self, create_room::{self, RoomPowerLevelsContentOverride, v3::CreationContent}, }, error::ErrorKind::Forbidden, }, assign, events::{ AnyStateEventContent, StateEventType, TimelineEventType, room::{ canonical_alias::RoomCanonicalAliasEventContent, create::RoomCreateEventContent, guest_access::{GuestAccess, RoomGuestAccessEventContent}, history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, join_rules::{JoinRule, RoomJoinRulesEventContent}, member::{MembershipState, RoomMemberEventContent}, name::RoomNameEventContent, power_levels::RoomPowerLevelsEventContent, server_acl::RoomServerAclEventContent, topic::RoomTopicEventContent, }, }, int, room_version_rules::{AuthorizationRules, RoomIdFormatVersion}, serde::Raw, }; use ruminuwuity::invite_permission_config::FilterLevel; use serde::Deserialize; use serde_json::value::to_raw_value; use crate::{Ruma, client::invite_helper}; static LAST_CREATE_TS: AtomicU64 = AtomicU64::new(0); // We use this to keep around extra data in the `creation_content` // while still parsing and validating keys that we care about. #[derive(Deserialize, Default)] struct FullCreationContent { #[serde(flatten)] creation_content: CreationContent, #[serde(flatten)] extra_create_keys: CanonicalJsonObject, } /// # `POST /_matrix/client/v3/createRoom` /// /// Creates a new room. /// /// - Room ID is randomly generated /// - Create alias if `room_alias_name` is set /// - Send create event /// - Join sender user /// - Send power levels event /// - Send canonical room alias /// - Send join rules /// - Send history visibility /// - Send guest access /// - Send events listed in initial state /// - Send events implied by `name` and `topic` /// - Send invite events #[allow(clippy::large_stack_frames)] #[allow(clippy::cognitive_complexity)] pub(crate) async fn create_room_route( State(services): State, body: Ruma, ) -> Result { use create_room::v3::RoomPreset; let sender_user = body.identity.expect_sender_user()?; if !services.globals.allow_room_creation() && !body.identity.is_appservice() && !services.users.is_admin(sender_user).await { return Err!(Request(Forbidden("Room creation has been disabled.",))); } if services.users.is_suspended(sender_user).await? { return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); } let room_version = match body.room_version.clone() { | Some(room_version) => if services.server.supported_room_version(&room_version) { room_version } else { return Err!(Request(UnsupportedRoomVersion( "This server does not support that room version." ))); }, | None => services.server.config.default_room_version.clone(), }; let room_version_rules = room_version.rules().unwrap(); // For custom room IDs, if the user is creating a room with a v1 room ID // format, we can just use that ID directly. However, if it's a custom *v2* // room ID, we need to make sure that we don't generate one, which would in // turn trick us into generating invalid v2 room events. // // expect_room_id is the custom room ID that the user is expecting - for v2 // formatted rooms, we check that the m.room.create event's generated room // ID exactly matches this, and abort if it doesn't. Otherwise, we use it // as the room ID itself. let expect_room_id = { let body_ref = body .json_body .as_ref() .ok_or_else(|| err!(Request(BadJson("Invalid JSON body"))))?; if let Some(CanonicalJsonValue::String(room_id)) = body_ref .get("fi.mau.room_id") .or_else(|| body_ref.get("room_id")) { Some( RoomId::parse(room_id) .map_err(|e| err!(Request(BadJson("Malformed custom room ID: {e}"))))?, ) } else { None } }; let room_id = match room_version_rules.room_id_format { | RoomIdFormatVersion::V1 => Some( expect_room_id .clone() .unwrap_or_else(|| RoomId::new_v1(services.globals.server_name())), ), | _ => None, }; // check if room ID doesn't already exist instead of erroring on auth check if let Some(room_id) = room_id.as_ref().or(expect_room_id.as_ref()) { if services.rooms.short.get_shortroomid(room_id).await.is_ok() { return Err!(Request(RoomInUse("Room with that custom room ID already exists",))); } } if body.visibility == room::Visibility::Public && services.server.config.lockdown_public_room_directory && !services.users.is_admin(sender_user).await && !body.identity.is_appservice() { warn!( "Non-admin user {sender_user} tried to publish {room_id:?} to the room directory \ while \"lockdown_public_room_directory\" is enabled" ); if services.server.config.admin_room_notices { services .admin .notice(&format!( "Non-admin user {sender_user} tried to publish {room_id:?} to the room \ directory while \"lockdown_public_room_directory\" is enabled" )) .await; } return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed"))); } // Figure out preset. We need it for preset specific events let preset = body.preset.clone().unwrap_or(match &body.visibility { | room::Visibility::Public => RoomPreset::PublicChat, | _ => RoomPreset::PrivateChat, // Room visibility should not be custom }); let mut invitees = BTreeSet::new(); for recipient_user in &body.invite { // silently drop invites to users the creator has ignored if services .users .user_is_ignored(recipient_user, sender_user) .await { continue; } // if the recipient of the invite is local and has the sender blocked, // error out. if the recipient is remote we can't tell yet, and if // they're local and have the sender _ignored_ their invite will be // filtered out in the handlers for the individual /sync endpoints if services.globals.user_is_local(recipient_user) && matches!( services .users .invite_filter_level(sender_user, recipient_user) .await, FilterLevel::Block ) { return Err!(Request(InviteBlocked( "{recipient_user} has blocked invites from you." ))); } invitees.insert(recipient_user.clone()); } let alias: Option = match body.room_alias_name.as_ref() { | Some(alias) => Some(room_alias_check(&services, alias, body.identity.appservice_info()).await?), | _ => None, }; let FullCreationContent { creation_content, mut extra_create_keys } = body .creation_content .as_ref() .map(Raw::deserialize_as_unchecked::) .transpose() .map_err(|e| err!(Request(BadJson(error!("Failed to parse creation content: {e}")))))? .unwrap_or_default(); let mut create_event_content = if room_version_rules.authorization.use_room_create_sender { RoomCreateEventContent::new_v11() } else { RoomCreateEventContent::new_v1(sender_user.to_owned()) }; let mut privileged_creators = HashSet::new(); create_event_content.federate = creation_content.federate; create_event_content.predecessor = creation_content.predecessor; create_event_content.room_type = creation_content.room_type; create_event_content.room_version = room_version.clone(); if room_version_rules.authorization.additional_room_creators { create_event_content.additional_creators = creation_content.additional_creators; // If we're making a trusted private chat, add the invitees as // additional creators if preset == RoomPreset::TrustedPrivateChat { create_event_content .additional_creators .extend(invitees.iter().cloned()); } // Make sure the primary creator of the room isn't in the additional // creator list if create_event_content .additional_creators .iter() .any(|user| user == sender_user) { return Err!(Request(InvalidParam( "The primary creator of the room may not also be an additional creator." ))); } // Sort and deduplicate additional creators to avoid any funny business create_event_content.additional_creators.sort(); create_event_content.additional_creators.dedup(); } // Track privileged creators so we can remove them from PLs later if room_version_rules .authorization .explicitly_privilege_room_creators { privileged_creators.insert(sender_user.to_owned()); if room_version_rules.authorization.additional_room_creators { privileged_creators.extend(create_event_content.additional_creators.iter().cloned()); } } let state_lock = match room_id.clone() { | Some(room_id) => { let _short_id = services .rooms .short .get_or_create_shortroomid(&room_id) .await; services.rooms.state.mutex.lock(room_id.as_str()).await }, | None => { let temp_room_id = RoomId::new_v1(services.globals.server_name()); trace!("Locking temporary room state mutex for {temp_room_id}"); services.rooms.state.mutex.lock(temp_room_id.as_str()).await }, }; // 1. The room create event debug!("Creating room create event for {sender_user} in room {room_id:?}"); let tmp_id = room_id.as_deref(); // Allow requesters to override the `origin_server_ts` to customize room ids // from v12 onwards let custom_origin_server_ts = { let body_ref = body .json_body .as_ref() .ok_or_else(|| err!(Request(BadJson("Invalid JSON body"))))?; body_ref .get("origin_server_ts") .or_else(|| body_ref.get("fi.mau.origin_server_ts")) .and_then(CanonicalJsonValue::as_integer) .map(Into::into) .and_then(|value: i64| value.try_into().ok()) .map(MilliSecondsSinceUnixEpoch) }; // v12+ room IDs are the create event's hash, so give each create event a // unique timestamp to avoid collisions. let create_timestamp = custom_origin_server_ts.or_else(|| { room_id.is_none().then(|| { let now = utils::millis_since_unix_epoch(); let next = |last: u64| now.max(last.saturating_add(1)); let prev = LAST_CREATE_TS.update(SeqCst, SeqCst, next); MilliSecondsSinceUnixEpoch(UInt::new_saturating(next(prev))) }) }); let mut create_event_content = to_canonical_object(create_event_content) .expect("should be able to serialize create event content"); // `extra_create_keys` can't clobber any important fields because we // already pulled them out into `create_event_content` create_event_content.append(&mut extra_create_keys); let create_event_id = services .rooms .timeline .build_and_append_pdu( PartialPdu { event_type: TimelineEventType::RoomCreate, content: to_raw_value(&create_event_content)?, state_key: Some(StateKey::new()), timestamp: create_timestamp, ..Default::default() }, sender_user, tmp_id, &state_lock, ) .boxed() .await?; trace!("Created room create event with ID {}", &create_event_id); let room_id = match room_id.clone() { | Some(room_id) => room_id, | None => { let as_room_id = create_event_id.as_str().replace('$', "!"); trace!("Creating room with v12 room ID {as_room_id}"); RoomId::parse(&as_room_id)?.clone() }, }; drop(state_lock); debug!("Room created with ID {room_id}"); if let Some(expected_room_id) = expect_room_id && expected_room_id != room_id { return Err!(BadServerResponse( "Room's final room ID was {room_id}, but expected {expected_room_id}" )); } let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await; // 2. Let the room creator join let mut join_event = RoomMemberEventContent::new(MembershipState::Join); join_event.displayname = services.users.displayname(sender_user).await.ok(); join_event.avatar_url = services.users.avatar_url(sender_user).await.ok(); join_event.is_direct = Some(body.is_direct); debug_info!("Joining {sender_user} to room {room_id}"); services .rooms .timeline .build_and_append_pdu( PartialPdu::state(sender_user.to_string(), &join_event), sender_user, Some(&room_id), &state_lock, ) .boxed() .await?; // 3. Power levels let mut power_levels_to_grant = BTreeMap::new(); // If this room version doesn't privilege creators, grant PLs to them if !room_version_rules .authorization .explicitly_privilege_room_creators { power_levels_to_grant.insert(sender_user.to_owned(), int!(100)); if preset == RoomPreset::TrustedPrivateChat { for recipient_user in invitees.iter().cloned() { power_levels_to_grant.insert(recipient_user, int!(100)); } } } let power_levels_content = default_power_levels_content( body.power_level_content_override.as_ref(), &body.visibility, power_levels_to_grant, privileged_creators, &room_version_rules.authorization, )?; services .rooms .timeline .build_and_append_pdu( PartialPdu { event_type: TimelineEventType::RoomPowerLevels, content: to_raw_value(&power_levels_content)?, state_key: Some(StateKey::new()), ..Default::default() }, sender_user, Some(&room_id), &state_lock, ) .boxed() .await?; // 4. Canonical room alias if let Some(room_alias_id) = &alias { services .rooms .timeline .build_and_append_pdu( PartialPdu::state( String::new(), &assign!(RoomCanonicalAliasEventContent::new(), { alias: Some(room_alias_id.to_owned()), alt_aliases: vec![], }), ), sender_user, Some(&room_id), &state_lock, ) .boxed() .await?; } // 5. Events set by preset // 5.1 Join Rules services .rooms .timeline .build_and_append_pdu( PartialPdu::state( String::new(), &RoomJoinRulesEventContent::new(match preset { | RoomPreset::PublicChat => JoinRule::Public, // according to spec "invite" is the default | _ => JoinRule::Invite, }), ), sender_user, Some(&room_id), &state_lock, ) .boxed() .await?; // 5.2 History Visibility services .rooms .timeline .build_and_append_pdu( PartialPdu::state( String::new(), &RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared), ), sender_user, Some(&room_id), &state_lock, ) .boxed() .await?; // 5.3 Guest Access services .rooms .timeline .build_and_append_pdu( PartialPdu::state( String::new(), &RoomGuestAccessEventContent::new(match preset { | RoomPreset::PublicChat => GuestAccess::Forbidden, | _ => GuestAccess::CanJoin, }), ), sender_user, Some(&room_id), &state_lock, ) .boxed() .await?; // 6. Initial state events provided by the homeserver let mut server_initial_state: Vec = Vec::new(); if let Some(allow_list) = services.server.config.default_room_acl_allow.clone() { server_initial_state.push(PartialPdu::state( String::new(), &RoomServerAclEventContent::new(true, allow_list, vec![]), )); } else if let Some(deny_list) = services.server.config.default_room_acl_deny.clone() { server_initial_state.push(PartialPdu::state( String::new(), &RoomServerAclEventContent::new(true, vec!["*".to_owned()], deny_list), )); } for pdu in server_initial_state { services .rooms .timeline .build_and_append_pdu(pdu, sender_user, Some(&room_id), &state_lock) .boxed() .await?; } // 7. Events listed in initial_state for event in &body.initial_state { let event_type: StateEventType = event.get_field("type")?.ok_or_else(|| { err!(Request(InvalidParam(warn!( r#"Invalid initial state event: missing "type" field "# )))) })?; let content: Raw = event.get_field("content")?.ok_or_else(|| { err!(Request(InvalidParam(warn!( r#"Invalid initial state event: missing "content" field "# )))) })?; let state_key: String = event.get_field("state_key")?.unwrap_or_default(); match services .rooms .timeline .send_state_event_for_key( sender_user, &room_id, &state_lock, &event_type, &content, &state_key, None, None, ) .boxed() .await { | Err(Error::Request(Forbidden, ..)) => { // Silently skip forbidden events continue; }, | r => r, }?; } // 8. Events implied by name and topic if let Some(name) = &body.name { services .rooms .timeline .build_and_append_pdu( PartialPdu::state(String::new(), &RoomNameEventContent::new(name.clone())), sender_user, Some(&room_id), &state_lock, ) .boxed() .await?; } if let Some(topic) = &body.topic { services .rooms .timeline .build_and_append_pdu( PartialPdu::state(String::new(), &RoomTopicEventContent::new(topic.clone())), sender_user, Some(&room_id), &state_lock, ) .boxed() .await?; } // 9. Events implied by invite (and TODO: invite_3pid) drop(state_lock); for recipient_user in &invitees { if let Err(e) = invite_helper(&services, sender_user, recipient_user, &room_id, None, body.is_direct) .boxed() .await { warn!(?e, "Failed to send invite"); } } // Homeserver specific stuff if let Some(alias) = alias { services .rooms .alias .set_alias(&alias, &room_id, sender_user)?; } if body.visibility == room::Visibility::Public { services.rooms.directory.set_public(&room_id); if services.server.config.admin_room_notices { services .admin .send_text(&format!("{sender_user} made {room_id} public to the room directory")) .await; } info!("{sender_user} made {0} public to the room directory", &room_id); } info!("{sender_user} created a room with room ID {room_id}"); Ok(create_room::v3::Response::new(room_id)) } /// creates the power_levels_content for the PDU builder #[allow(clippy::or_fun_call)] fn default_power_levels_content( power_level_content_override: Option<&Raw>, visibility: &room::Visibility, mut users: BTreeMap, creators: HashSet, authorization_rules: &AuthorizationRules, ) -> Result { use ruma::events::TimelineEventType::*; let mut power_levels_content = power_level_content_override .map(Raw::cast_ref::) .map(Raw::deserialize) .transpose() .map_err(|err| err!(Request(BadJson("Invalid power_level_content_override: {err:?}"))))? .map_or_else( || RoomPowerLevelsEventContent::new(authorization_rules), |mut content| { if authorization_rules.explicitly_privilege_room_creators { // If an override was supplied we need to make sure the // tombstone default is raised to 150 // for v12+. Otherwise, if one wasn't supplied, // `RoomPowerLevelsEventContent::new` will handle this. content.events.entry(RoomTombstone).or_insert(int!(150)); } content }, ); power_levels_content.users.append(&mut users); for creator in creators { power_levels_content.users.remove(&creator); } // secure proper defaults of sensitive/dangerous permissions that moderators // (power level 50) should not have easy access to let admin_only_events = [ RoomPowerLevels, RoomServerAcl, // this will not apply for v12+ rooms since we raised it to 150 earlier RoomTombstone, RoomEncryption, RoomHistoryVisibility, ]; for event_type in admin_only_events { power_levels_content .events .entry(event_type) .or_insert(int!(100)); } // always allow users to respond (not post new) to polls. this is primarily // useful in read-only announcement rooms that post a public poll. power_levels_content .events .entry(PollResponse) .or_insert(int!(0)); power_levels_content .events .entry(UnstablePollResponse) .or_insert(int!(0)); // synapse does this too. clients do not expose these permissions. it // prevents default users from calling public rooms, for obvious reasons. if *visibility == room::Visibility::Public { power_levels_content .events .entry(CallInvite) .or_insert(int!(50)); power_levels_content .events .entry(CallMember) .or_insert(int!(50)); } Ok(power_levels_content) } /// if a room is being created with a room alias, run our checks async fn room_alias_check( services: &Services, room_alias_name: &str, appservice_info: Option<&RegistrationInfo>, ) -> Result { // Basic checks on the room alias validity if room_alias_name.contains(':') { return Err!(Request(InvalidParam( "Room alias contained `:` which is not allowed. Please note that this expects a \ localpart, not the full room alias.", ))); } else if room_alias_name.contains(char::is_whitespace) { return Err!(Request(InvalidParam( "Room alias contained spaces which is not a valid room alias.", ))); } // check if room alias is forbidden if services .globals .forbidden_alias_names() .is_match(room_alias_name) { return Err!(Request(Unknown("Room alias name is forbidden."))); } let server_name = services.globals.server_name(); let full_room_alias = RoomAliasId::parse(format!("#{room_alias_name}:{server_name}")) .map_err(|e| { err!(Request(InvalidParam(debug_error!( ?e, %room_alias_name, "Failed to parse room alias.", )))) })?; if services .rooms .alias .resolve_local_alias(&full_room_alias) .await .is_ok() { return Err!(Request(RoomInUse("Room alias already exists."))); } if let Some(info) = appservice_info { if !info.aliases.is_match(full_room_alias.as_str()) { return Err!(Request(Exclusive("Room alias is not in namespace."))); } } else if services.appservice.is_exclusive_alias(&full_room_alias) { return Err!(Request(Exclusive("Room alias reserved by appservice.",))); } debug_info!("Full room alias: {full_room_alias}"); Ok(full_room_alias) }