Files
continuwuity/src/api/client/room/create.rs
T

791 lines
22 KiB
Rust

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<crate::State>,
body: Ruma<create_room::v3::Request>,
) -> Result<create_room::v3::Response> {
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<OwnedRoomAliasId> = 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::<FullCreationContent>)
.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<PartialPdu> = 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<AnyStateEventContent> =
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<RoomPowerLevelsContentOverride>>,
visibility: &room::Visibility,
mut users: BTreeMap<OwnedUserId, Int>,
creators: HashSet<OwnedUserId>,
authorization_rules: &AuthorizationRules,
) -> Result<RoomPowerLevelsEventContent> {
use ruma::events::TimelineEventType::*;
let mut power_levels_content = power_level_content_override
.map(Raw::cast_ref::<RoomPowerLevelsEventContent>)
.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<OwnedRoomAliasId> {
// 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)
}