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

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

651 lines
18 KiB
Rust
Raw Normal View History

2024-11-06 18:27:40 +00:00
use std::collections::BTreeMap;
2024-03-05 19:48:54 -05:00
2024-07-16 08:05:25 +00:00
use axum::extract::State;
2024-12-28 23:31:24 +00:00
use conduwuit::{
Err, Error, Result, debug_info, debug_warn, err, error, info,
matrix::{StateKey, pdu::PduBuilder},
warn,
2024-12-28 23:31:24 +00:00
};
use conduwuit_service::{Services, appservice::RegistrationInfo};
2024-11-06 18:27:40 +00:00
use futures::FutureExt;
2020-07-30 18:14:47 +02:00
use ruma::{
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
2020-07-30 18:14:47 +02:00
api::client::{
error::ErrorKind,
2024-11-06 18:27:40 +00:00
room::{self, create_room},
2020-07-30 18:14:47 +02:00
},
events::{
TimelineEventType,
2021-10-13 10:16:45 +02:00
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,
topic::RoomTopicEventContent,
},
2020-07-30 18:14:47 +02:00
},
2022-10-10 14:09:11 +02:00
int,
serde::{JsonObject, Raw},
2020-07-30 18:14:47 +02:00
};
use serde_json::{json, value::to_raw_value};
2020-07-30 18:14:47 +02:00
use crate::{Ruma, client::invite_helper};
2024-03-05 19:48:54 -05:00
/// # `POST /_matrix/client/v3/createRoom`
2021-08-31 19:14:37 +02:00
///
/// Creates a new room.
///
/// - Room ID is randomly generated
/// - Create alias if `room_alias_name` is set
2021-08-31 19:14:37 +02:00
/// - 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)]
2024-07-16 08:05:25 +00:00
pub(crate) async fn create_room_route(
State(services): State<crate::State>,
body: Ruma<create_room::v3::Request>,
2024-07-16 08:05:25 +00:00
) -> Result<create_room::v3::Response> {
2022-02-18 15:33:14 +01:00
use create_room::v3::RoomPreset;
2024-03-05 19:48:54 -05:00
let sender_user = body.sender_user();
2024-03-05 19:48:54 -05:00
2024-07-16 08:05:25 +00:00
if !services.globals.allow_room_creation()
&& body.appservice_info.is_none()
2024-08-08 17:18:30 +00:00
&& !services.users.is_admin(sender_user).await
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Room creation has been disabled.",
));
2024-01-24 12:18:49 -05:00
}
2024-03-05 19:48:54 -05:00
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let room_id: OwnedRoomId = match &body.room_id {
| Some(custom_room_id) => custom_room_id_check(&services, custom_room_id)?,
| _ => RoomId::new(&services.server.name),
};
2024-03-05 19:48:54 -05:00
// check if room ID doesn't already exist instead of erroring on auth check
2024-08-08 17:18:30 +00:00
if services.rooms.short.get_shortroomid(&room_id).await.is_ok() {
return Err(Error::BadRequest(
ErrorKind::RoomInUse,
"Room with that custom room ID already exists",
));
}
2024-03-05 19:48:54 -05:00
if body.visibility == room::Visibility::Public
&& services.server.config.lockdown_public_room_directory
2024-08-08 17:18:30 +00:00
&& !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none()
{
info!(
"Non-admin user {sender_user} tried to publish {0} to the room directory while \
\"lockdown_public_room_directory\" is enabled",
&room_id
);
if services.server.config.admin_room_notices {
services
.admin
.send_text(&format!(
"Non-admin user {sender_user} tried to publish {0} to the room directory \
while \"lockdown_public_room_directory\" is enabled",
&room_id
))
.await;
}
return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")));
}
2024-08-08 17:18:30 +00:00
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&room_id)
.await;
2024-07-16 08:05:25 +00:00
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
2024-03-05 19:48:54 -05:00
let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() {
| Some(alias) =>
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?),
| _ => None,
};
2024-01-24 16:44:37 -05:00
let room_version = match body.room_version.clone() {
| Some(room_version) =>
2024-12-05 07:23:51 +00:00
if services.server.supported_room_version(&room_version) {
room_version
} else {
return Err(Error::BadRequest(
ErrorKind::UnsupportedRoomVersion,
"This server does not support that room version.",
));
},
| None => services.server.config.default_room_version.clone(),
2024-03-05 19:48:54 -05:00
};
let create_content = match &body.creation_content {
| Some(content) => {
2024-07-12 01:08:53 +00:00
use RoomVersionId::*;
2024-03-25 17:05:11 -04:00
let mut content = content
.deserialize_as::<CanonicalJsonObject>()
.map_err(|e| {
error!("Failed to deserialise content as canonical JSON: {}", e);
Error::bad_database("Failed to deserialise content as canonical JSON.")
})?;
2022-10-05 20:34:31 +02:00
match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|e| {
info!("Invalid creation content: {e}");
Error::BadRequest(ErrorKind::BadJson, "Invalid creation content")
2024-03-05 19:48:54 -05:00
})?,
);
},
| _ => {
// V11+ removed the "creator" key
2021-08-07 15:55:03 +02:00
},
}
2022-09-06 23:15:09 +02:00
content.insert(
"room_version".into(),
json!(room_version.as_str()).try_into().map_err(|_| {
Error::BadRequest(ErrorKind::BadJson, "Invalid creation content")
})?,
2024-03-05 19:48:54 -05:00
);
content
},
| None => {
2024-07-12 01:08:53 +00:00
use RoomVersionId::*;
let content = match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(sender_user.to_owned()),
| _ => RoomCreateEventContent::new_v11(),
2024-03-05 19:48:54 -05:00
};
let mut content = serde_json::from_str::<CanonicalJsonObject>(
to_raw_value(&content)
.expect("we just created this as content was None")
.get(),
)
.unwrap();
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.expect("we just created this as content was None"),
2024-03-05 19:48:54 -05:00
);
content
2023-12-24 19:04:48 +01:00
},
2024-03-05 19:48:54 -05:00
};
// 1. The room create event
2024-07-16 08:05:25 +00:00
services
2024-03-05 19:48:54 -05:00
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)
.expect("create event content serialization"),
state_key: Some(StateKey::new()),
..Default::default()
2024-03-05 19:48:54 -05:00
},
sender_user,
&room_id,
&state_lock,
)
2024-08-08 17:18:30 +00:00
.boxed()
.await?;
2024-03-05 19:48:54 -05:00
2020-07-30 18:14:47 +02:00
// 2. Let the room creator join
2024-07-16 08:05:25 +00:00
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(sender_user.to_string(), &RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
is_direct: Some(body.is_direct),
..RoomMemberEventContent::new(MembershipState::Join)
}),
sender_user,
&room_id,
&state_lock,
)
2024-08-08 17:18:30 +00:00
.boxed()
.await?;
2024-03-05 19:48:54 -05:00
2020-07-30 18:14:47 +02:00
// 3. Power levels
2024-03-05 19:48:54 -05:00
// Figure out preset. We need it for preset specific events
2022-10-10 14:09:11 +02:00
let preset = body.preset.clone().unwrap_or(match &body.visibility {
| room::Visibility::Public => RoomPreset::PublicChat,
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
2024-03-05 19:48:54 -05:00
});
let mut users = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
2024-03-05 19:48:54 -05:00
if preset == RoomPreset::TrustedPrivateChat {
for invite in &body.invite {
if services.users.user_is_ignored(sender_user, invite).await {
continue;
} else if services.users.user_is_ignored(invite, sender_user).await {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
continue;
}
users.insert(invite.clone(), int!(100));
2024-03-05 19:48:54 -05:00
}
}
let power_levels_content = default_power_levels_content(
body.power_level_content_override.as_ref(),
&body.visibility,
users,
)?;
2024-03-05 19:48:54 -05:00
2024-07-16 08:05:25 +00:00
services
2024-03-05 19:48:54 -05:00
.rooms
2021-11-27 17:44:52 +01:00
.timeline
.build_and_append_pdu(
2021-10-13 10:16:45 +02:00
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_content)
.expect("serialized power_levels event content"),
state_key: Some(StateKey::new()),
..Default::default()
2024-03-05 19:48:54 -05:00
},
2021-10-13 11:51:30 +02:00
sender_user,
&room_id,
&state_lock,
2024-03-05 19:48:54 -05:00
)
2024-08-08 17:18:30 +00:00
.boxed()
2024-03-05 19:48:54 -05:00
.await?;
2021-10-13 11:51:30 +02:00
// 4. Canonical room alias
if let Some(room_alias_id) = &alias {
2024-07-16 08:05:25 +00:00
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomCanonicalAliasEventContent {
alias: Some(room_alias_id.to_owned()),
alt_aliases: vec![],
}),
sender_user,
&room_id,
&state_lock,
)
2024-08-08 17:18:30 +00:00
.boxed()
.await?;
2024-03-05 19:48:54 -05:00
}
// 5. Events set by preset
2024-03-05 19:48:54 -05:00
// 5.1 Join Rules
2024-07-16 08:05:25 +00:00
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(
String::new(),
&RoomJoinRulesEventContent::new(match preset {
| RoomPreset::PublicChat => JoinRule::Public,
// according to spec "invite" is the default
| _ => JoinRule::Invite,
}),
),
sender_user,
&room_id,
&state_lock,
2024-03-05 19:48:54 -05:00
)
2024-08-08 17:18:30 +00:00
.boxed()
2024-03-05 19:48:54 -05:00
.await?;
// 5.2 History Visibility
2024-07-16 08:05:25 +00:00
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(
String::new(),
&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared),
),
sender_user,
&room_id,
&state_lock,
)
2024-08-08 17:18:30 +00:00
.boxed()
.await?;
2024-03-05 19:48:54 -05:00
// 5.3 Guest Access
2024-07-16 08:05:25 +00:00
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(
String::new(),
&RoomGuestAccessEventContent::new(match preset {
| RoomPreset::PublicChat => GuestAccess::Forbidden,
| _ => GuestAccess::CanJoin,
}),
),
sender_user,
&room_id,
&state_lock,
)
2024-08-08 17:18:30 +00:00
.boxed()
.await?;
2024-03-05 19:48:54 -05:00
// 6. Events listed in initial_state
2020-10-05 22:19:22 +02:00
for event in &body.initial_state {
let mut pdu_builder = event.deserialize_as::<PduBuilder>().map_err(|e| {
warn!("Invalid initial state event: {:?}", e);
Error::BadRequest(ErrorKind::InvalidParam, "Invalid initial state event.")
})?;
2024-03-05 19:48:54 -05:00
2024-05-23 01:27:04 -04:00
debug_info!("Room creation initial state event: {event:?}");
// client/appservice workaround: if a user sends an initial_state event with a
// state event in there with the content of literally `{}` (not null or empty
// string), let's just skip it over and warn.
if pdu_builder.content.get().eq("{}") {
info!("skipping empty initial state event with content of `{{}}`: {event:?}");
debug_warn!("content: {}", pdu_builder.content.get());
continue;
}
// Implicit state key defaults to ""
pdu_builder.state_key.get_or_insert_with(StateKey::new);
2024-03-05 19:48:54 -05:00
2020-10-05 22:19:22 +02:00
// Silently skip encryption events if they are not allowed
if pdu_builder.event_type == TimelineEventType::RoomEncryption
&& !services.config.allow_encryption
{
2020-10-05 22:19:22 +02:00
continue;
}
2024-03-05 19:48:54 -05:00
2024-07-16 08:05:25 +00:00
services
2024-03-25 17:05:11 -04:00
.rooms
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
2024-08-08 17:18:30 +00:00
.boxed()
2024-03-25 17:05:11 -04:00
.await?;
2020-10-05 22:19:22 +02:00
}
2024-03-05 19:48:54 -05:00
// 7. Events implied by name and topic
2020-10-05 22:19:22 +02:00
if let Some(name) = &body.name {
2024-07-16 08:05:25 +00:00
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomNameEventContent::new(name.clone())),
sender_user,
&room_id,
&state_lock,
)
2024-08-08 17:18:30 +00:00
.boxed()
.await?;
2020-10-05 22:19:22 +02:00
}
2024-03-05 19:48:54 -05:00
2020-10-05 22:19:22 +02:00
if let Some(topic) = &body.topic {
2024-07-16 08:05:25 +00:00
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }),
sender_user,
&room_id,
&state_lock,
)
2024-08-08 17:18:30 +00:00
.boxed()
.await?;
2020-07-30 18:14:47 +02:00
}
2024-03-05 19:48:54 -05:00
// 8. Events implied by invite (and TODO: invite_3pid)
2021-08-03 11:10:58 +02:00
drop(state_lock);
2021-04-25 14:10:07 +02:00
for user_id in &body.invite {
if services.users.user_is_ignored(sender_user, user_id).await {
continue;
} else if services.users.user_is_ignored(user_id, sender_user).await {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
continue;
}
if let Err(e) =
invite_helper(&services, sender_user, user_id, &room_id, None, body.is_direct)
.boxed()
.await
2024-08-08 17:18:30 +00:00
{
2024-05-23 01:27:04 -04:00
warn!(%e, "Failed to send invite");
}
2020-07-30 18:14:47 +02:00
}
2024-03-05 19:48:54 -05:00
2020-07-30 18:14:47 +02:00
// Homeserver specific stuff
if let Some(alias) = alias {
2024-07-16 08:05:25 +00:00
services
2024-06-12 01:42:39 -04:00
.rooms
.alias
.set_alias(&alias, &room_id, sender_user)?;
2020-07-30 18:14:47 +02:00
}
2024-03-05 19:48:54 -05:00
2020-09-08 17:32:03 +02:00
if body.visibility == room::Visibility::Public {
2024-08-08 17:18:30 +00:00
services.rooms.directory.set_public(&room_id);
if services.server.config.admin_room_notices {
services
.admin
.send_text(&format!(
"{sender_user} made {} public to the room directory",
&room_id
))
.await;
}
info!("{sender_user} made {0} public to the room directory", &room_id);
2020-07-30 18:14:47 +02:00
}
2024-03-05 19:48:54 -05:00
info!("{sender_user} created a room with room ID {room_id}");
2024-03-05 19:48:54 -05:00
2022-02-18 15:33:14 +01:00
Ok(create_room::v3::Response::new(room_id))
2020-07-30 18:14:47 +02:00
}
/// creates the power_levels_content for the PDU builder
fn default_power_levels_content(
power_level_content_override: Option<&Raw<RoomPowerLevelsEventContent>>,
visibility: &room::Visibility,
users: BTreeMap<OwnedUserId, Int>,
) -> Result<serde_json::Value> {
let mut power_levels_content =
serde_json::to_value(RoomPowerLevelsEventContent { users, ..Default::default() })
.expect("event is valid, we just created it");
// secure proper defaults of sensitive/dangerous permissions that moderators
// (power level 50) should not have easy access to
power_levels_content["events"]["m.room.power_levels"] =
serde_json::to_value(100).expect("100 is valid Value");
power_levels_content["events"]["m.room.server_acl"] =
serde_json::to_value(100).expect("100 is valid Value");
power_levels_content["events"]["m.room.tombstone"] =
serde_json::to_value(100).expect("100 is valid Value");
power_levels_content["events"]["m.room.encryption"] =
serde_json::to_value(100).expect("100 is valid Value");
power_levels_content["events"]["m.room.history_visibility"] =
serde_json::to_value(100).expect("100 is valid Value");
// 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"]["org.matrix.msc3381.poll.response"] =
serde_json::to_value(0).expect("0 is valid Value");
power_levels_content["events"]["m.poll.response"] =
serde_json::to_value(0).expect("0 is valid Value");
// 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"]["m.call.invite"] =
serde_json::to_value(50).expect("50 is valid Value");
power_levels_content["events"]["m.call"] =
serde_json::to_value(50).expect("50 is valid Value");
power_levels_content["events"]["m.call.member"] =
serde_json::to_value(50).expect("50 is valid Value");
power_levels_content["events"]["org.matrix.msc3401.call"] =
serde_json::to_value(50).expect("50 is valid Value");
power_levels_content["events"]["org.matrix.msc3401.call.member"] =
serde_json::to_value(50).expect("50 is valid Value");
}
if let Some(power_level_content_override) = power_level_content_override {
let json: JsonObject = serde_json::from_str(power_level_content_override.json().get())
.map_err(|_| {
Error::BadRequest(ErrorKind::BadJson, "Invalid power_level_content_override.")
})?;
for (key, value) in json {
power_levels_content[key] = value;
}
}
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(Error::BadRequest(
ErrorKind::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(Error::BadRequest(
ErrorKind::InvalidParam,
"Room alias contained spaces which is not a valid room alias.",
));
}
// check if room alias is forbidden
2024-07-16 08:05:25 +00:00
if services
.globals
.forbidden_alias_names()
.is_match(room_alias_name)
{
return Err(Error::BadRequest(ErrorKind::Unknown, "Room alias name is forbidden."));
}
2024-12-28 23:31:24 +00:00
let server_name = services.globals.server_name();
let full_room_alias = OwnedRoomAliasId::parse(format!("#{room_alias_name}:{server_name}"))
.map_err(|e| {
err!(Request(InvalidParam(debug_error!(
?e,
?room_alias_name,
"Failed to parse room alias.",
))))
})?;
2024-07-16 08:05:25 +00:00
if services
.rooms
.alias
2024-08-08 17:18:30 +00:00
.resolve_local_alias(&full_room_alias)
.await
.is_ok()
{
return Err(Error::BadRequest(ErrorKind::RoomInUse, "Room alias already exists."));
}
2024-10-27 00:30:30 +00:00
if let Some(info) = appservice_info {
if !info.aliases.is_match(full_room_alias.as_str()) {
return Err(Error::BadRequest(
ErrorKind::Exclusive,
"Room alias is not in namespace.",
));
}
2024-07-16 08:05:25 +00:00
} else if services
.appservice
.is_exclusive_alias(&full_room_alias)
.await
{
return Err(Error::BadRequest(
ErrorKind::Exclusive,
"Room alias reserved by appservice.",
));
}
debug_info!("Full room alias: {full_room_alias}");
Ok(full_room_alias)
}
/// if a room is being created with a custom room ID, run our checks against it
2024-07-16 08:05:25 +00:00
fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
// apply forbidden room alias checks to custom room IDs too
2024-07-16 08:05:25 +00:00
if services
.globals
.forbidden_alias_names()
.is_match(custom_room_id)
{
return Err(Error::BadRequest(ErrorKind::Unknown, "Custom room ID is forbidden."));
}
let server_name = services.globals.server_name();
let mut room_id = custom_room_id.to_owned();
if custom_room_id.contains(':') {
if !custom_room_id.starts_with('!') {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Custom room ID contains an unexpected `:` which is not allowed.",
));
}
2025-04-23 17:48:33 +01:00
} else if custom_room_id.starts_with('!') {
return Err(Error::BadRequest(
2025-04-23 17:48:33 +01:00
ErrorKind::InvalidParam,
"Room ID is prefixed with !, but is not fully qualified. You likely did not want \
this.",
));
} else {
room_id = format!("!{custom_room_id}:{server_name}");
}
OwnedRoomId::parse(room_id)
2024-12-28 23:31:24 +00:00
.map_err(Into::into)
2025-04-23 17:48:33 +01:00
.and_then(|full_room_id| {
if full_room_id
.server_name()
.expect("failed to extract server name from room ID")
!= server_name
{
Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Custom room ID must be on this server.",
))
} else {
Ok(full_room_id)
}
2025-04-23 17:48:33 +01:00
})
.inspect(|full_room_id| {
debug_info!(?full_room_id, "Full custom room ID");
})
2024-12-28 23:31:24 +00:00
.inspect_err(|e| warn!(?e, ?custom_room_id, "Failed to create room with custom room ID",))
}