grapevine/src/api/client_server/room.rs

866 lines
28 KiB
Rust

use std::{cmp::max, collections::BTreeMap};
use ruma::{
api::client::{
error::ErrorKind,
room::{self, aliases, create_room, get_room_event, upgrade_room},
},
events::{
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,
tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent,
},
StateEventType, TimelineEventType,
},
int,
serde::JsonObject,
CanonicalJsonObject, OwnedRoomAliasId, RoomAliasId, RoomId, RoomVersionId,
};
use serde_json::{json, value::to_raw_value};
use tracing::{info, warn};
use crate::{
api::client_server::invite_helper, service::pdu::PduBuilder, services, Ar,
Error, Ra, Result,
};
/// # `POST /_matrix/client/r0/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::too_many_lines)]
pub(crate) async fn create_room_route(
body: Ar<create_room::v3::Request>,
) -> Result<Ra<create_room::v3::Response>> {
use create_room::v3::RoomPreset;
let sender_user =
body.sender_user.as_deref().expect("user is authenticated");
let room_id = RoomId::new(services().globals.server_name());
services().rooms.short.get_or_create_shortroomid(&room_id)?;
let room_token =
services().globals.roomid_mutex_state.lock_key(room_id.clone()).await;
if !services().globals.allow_room_creation()
&& body.appservice_info.is_none()
&& !services().users.is_admin(sender_user)?
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Room creation has been disabled.",
));
}
let alias: Option<OwnedRoomAliasId> =
body.room_alias_name.as_ref().map_or(Ok(None), |localpart| {
// TODO: Check for invalid characters and maximum length
let alias = RoomAliasId::parse(format!(
"#{}:{}",
localpart,
services().globals.server_name()
))
.map_err(|_| {
Error::BadRequest(ErrorKind::InvalidParam, "Invalid alias.")
})?;
if services().rooms.alias.resolve_local_alias(&alias)?.is_some() {
Err(Error::BadRequest(
ErrorKind::RoomInUse,
"Room alias already exists.",
))
} else {
Ok(Some(alias))
}
})?;
if let Some(alias) = &alias {
if let Some(info) = &body.appservice_info {
if !info.aliases.is_match(alias.as_str()) {
return Err(Error::BadRequest(
ErrorKind::Exclusive,
"Room alias is not in namespace.",
));
}
} else if services().appservice.is_exclusive_alias(alias).await {
return Err(Error::BadRequest(
ErrorKind::Exclusive,
"Room alias reserved by appservice.",
));
}
}
let room_version = match body.room_version.clone() {
Some(room_version) => {
if services()
.globals
.supported_room_versions()
.contains(&room_version)
{
room_version
} else {
return Err(Error::BadRequest(
ErrorKind::UnsupportedRoomVersion,
"This server does not support that room version.",
));
}
}
None => services().globals.default_room_version(),
};
let content = match &body.creation_content {
Some(content) => {
let mut content = content
.deserialize_as::<CanonicalJsonObject>()
.expect("Invalid creation content");
match &room_version {
room_version if *room_version < RoomVersionId::V11 => {
content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|_| {
Error::BadRequest(
ErrorKind::BadJson,
"Invalid creation content",
)
})?,
);
}
// V11 removed the "creator" key
RoomVersionId::V11 => {}
_ => {
return Err(Error::BadServerResponse(
"Unsupported room version.",
))
}
}
content.insert(
"room_version".into(),
json!(room_version.as_str()).try_into().map_err(|_| {
Error::BadRequest(
ErrorKind::BadJson,
"Invalid creation content",
)
})?,
);
content
}
None => {
let content = match &room_version {
room_version if *room_version < RoomVersionId::V11 => {
RoomCreateEventContent::new_v1(sender_user.to_owned())
}
RoomVersionId::V11 => RoomCreateEventContent::new_v11(),
_ => {
return Err(Error::BadServerResponse(
"Unsupported room version.",
))
}
};
let mut content = serde_json::from_str::<CanonicalJsonObject>(
to_raw_value(&content)
.map_err(|_| {
Error::BadRequest(
ErrorKind::BadJson,
"Invalid creation content",
)
})?
.get(),
)
.unwrap();
content.insert(
"room_version".into(),
json!(room_version.as_str()).try_into().map_err(|_| {
Error::BadRequest(
ErrorKind::BadJson,
"Invalid creation content",
)
})?,
);
content
}
};
// Validate creation content
let de_result = serde_json::from_str::<CanonicalJsonObject>(
to_raw_value(&content).expect("Invalid creation content").get(),
);
if de_result.is_err() {
return Err(Error::BadRequest(
ErrorKind::BadJson,
"Invalid creation content",
));
}
// 1. The room create event
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&content)
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&room_token,
)
.await?;
// 2. Let the room creator join
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: services().users.displayname(sender_user)?,
avatar_url: services().users.avatar_url(sender_user)?,
is_direct: Some(body.is_direct),
third_party_invite: None,
blurhash: services().users.blurhash(sender_user)?,
reason: None,
join_authorized_via_users_server: None,
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.to_string()),
redacts: None,
},
sender_user,
&room_token,
)
.await?;
// 3. Power levels
// Figure out preset. We need it for preset specific events
let preset = body.preset.clone().unwrap_or(match &body.visibility {
room::Visibility::Private => RoomPreset::PrivateChat,
room::Visibility::Public => RoomPreset::PublicChat,
_ => unimplemented!("unknown room visibility"),
});
let mut users = BTreeMap::new();
users.insert(sender_user.to_owned(), int!(100));
if preset == RoomPreset::TrustedPrivateChat {
for invite_ in &body.invite {
users.insert(invite_.clone(), int!(100));
}
}
let mut power_levels_content =
serde_json::to_value(RoomPowerLevelsEventContent {
users,
..Default::default()
})
.expect("event is valid, we just created it");
if let Some(power_level_content_override) =
&body.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;
}
}
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_content)
.expect("to_raw_value always works on serde_json::Value"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&room_token,
)
.await?;
// 4. Canonical room alias
if let Some(room_alias_id) = &alias {
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCanonicalAlias,
content: to_raw_value(&RoomCanonicalAliasEventContent {
alias: Some(room_alias_id.to_owned()),
alt_aliases: vec![],
})
.expect("We checked that alias earlier, it must be fine"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&room_token,
)
.await?;
}
// 5. Events set by preset
// 5.1 Join Rules
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomJoinRules,
content: to_raw_value(&RoomJoinRulesEventContent::new(
match preset {
RoomPreset::PublicChat => JoinRule::Public,
// according to spec "invite" is the default
_ => JoinRule::Invite,
},
))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&room_token,
)
.await?;
// 5.2 History Visibility
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomHistoryVisibility,
content: to_raw_value(&RoomHistoryVisibilityEventContent::new(
HistoryVisibility::Shared,
))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&room_token,
)
.await?;
// 5.3 Guest Access
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomGuestAccess,
content: to_raw_value(&RoomGuestAccessEventContent::new(
match preset {
RoomPreset::PublicChat => GuestAccess::Forbidden,
_ => GuestAccess::CanJoin,
},
))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&room_token,
)
.await?;
// 6. Events listed in initial_state
for event in &body.initial_state {
let mut pdu_builder =
event.deserialize_as::<PduBuilder>().map_err(|error| {
warn!(%error, "Invalid initial state event");
Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid initial state event.",
)
})?;
// Implicit state key defaults to ""
pdu_builder.state_key.get_or_insert_with(String::new);
// Silently skip encryption events if they are not allowed
if pdu_builder.event_type == TimelineEventType::RoomEncryption
&& !services().globals.allow_encryption()
{
continue;
}
services()
.rooms
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_token)
.await?;
}
// 7. Events implied by name and topic
if let Some(name) = &body.name {
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomName,
content: to_raw_value(&RoomNameEventContent::new(
name.clone(),
))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&room_token,
)
.await?;
}
if let Some(topic) = &body.topic {
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomTopic,
content: to_raw_value(&RoomTopicEventContent {
topic: topic.clone(),
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&room_token,
)
.await?;
}
// 8. Events implied by invite (and TODO: invite_3pid)
drop(room_token);
for user_id in &body.invite {
if let Err(error) =
invite_helper(sender_user, user_id, &room_id, None, body.is_direct)
.await
{
warn!(%error, "Invite helper failed");
};
}
// 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)?;
}
info!(user_id = %sender_user, room_id = %room_id, "User created a room");
Ok(Ra(create_room::v3::Response::new(room_id)))
}
/// # `GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}`
///
/// Gets a single event.
///
/// - You have to currently be joined to the room (TODO: Respect history
/// visibility)
pub(crate) async fn get_room_event_route(
body: Ar<get_room_event::v3::Request>,
) -> Result<Ra<get_room_event::v3::Response>> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event = services().rooms.timeline.get_pdu(&body.event_id)?.ok_or_else(
|| {
warn!(event_id = %body.event_id, "Event not found");
Error::BadRequest(ErrorKind::NotFound, "Event not found.")
},
)?;
if !services().rooms.state_accessor.user_can_see_event(
sender_user,
&event.room_id,
&body.event_id,
)? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view this event.",
));
}
let mut event = (*event).clone();
event.add_age()?;
Ok(Ra(get_room_event::v3::Response {
event: event.to_room_event(),
}))
}
/// # `GET /_matrix/client/r0/rooms/{roomId}/aliases`
///
/// Lists all aliases of the room.
///
/// - Only users joined to the room are allowed to call this TODO: Allow any
/// user to call it if `history_visibility` is world readable
pub(crate) async fn get_room_aliases_route(
body: Ar<aliases::v3::Request>,
) -> Result<Ra<aliases::v3::Response>> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services().rooms.state_cache.is_joined(sender_user, &body.room_id)? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view this room.",
));
}
Ok(Ra(aliases::v3::Response {
aliases: services()
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.filter_map(Result::ok)
.collect(),
}))
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
///
/// Upgrades the room.
///
/// - Creates a replacement room
/// - Sends a tombstone event into the current room
/// - Sender user joins the room
/// - Transfers some state events
/// - Moves local aliases
/// - Modifies old room power levels to prevent users from speaking
#[allow(clippy::too_many_lines)]
pub(crate) async fn upgrade_room_route(
body: Ar<upgrade_room::v3::Request>,
) -> Result<Ra<upgrade_room::v3::Response>> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services().globals.supported_room_versions().contains(&body.new_version)
{
return Err(Error::BadRequest(
ErrorKind::UnsupportedRoomVersion,
"This server does not support that room version.",
));
}
// Create a replacement room
let replacement_room = RoomId::new(services().globals.server_name());
services().rooms.short.get_or_create_shortroomid(&replacement_room)?;
let original_room_token = services()
.globals
.roomid_mutex_state
.lock_key(body.room_id.clone())
.await;
// Send a m.room.tombstone event to the old room to indicate that it is not
// intended to be used any further Fail if the sender does not have the
// required permissions
let tombstone_event_id = services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomTombstone,
content: to_raw_value(&RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: replacement_room.clone(),
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&original_room_token,
)
.await?;
// Change lock to replacement room
let replacement_room_token = services()
.globals
.roomid_mutex_state
.lock_key(replacement_room.clone())
.await;
// Get the old room creation event
let mut create_event_content = serde_json::from_str::<CanonicalJsonObject>(
services()
.rooms
.state_accessor
.room_state_get(&body.room_id, &StateEventType::RoomCreate, "")?
.ok_or_else(|| {
Error::bad_database("Found room without m.room.create event.")
})?
.content
.get(),
)
.map_err(|_| Error::bad_database("Invalid room event in database."))?;
// Use the m.room.tombstone event as the predecessor
let predecessor = Some(ruma::events::room::create::PreviousRoom::new(
body.room_id.clone(),
(*tombstone_event_id).to_owned(),
));
// Send a m.room.create event containing a predecessor field and the
// applicable room_version
match &body.new_version {
room_version if *room_version < RoomVersionId::V11 => {
create_event_content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|_| {
Error::BadRequest(
ErrorKind::BadJson,
"Error forming creation event",
)
})?,
);
}
RoomVersionId::V11 => {
// "creator" key no longer exists in V11 rooms
create_event_content.remove("creator");
}
_ => return Err(Error::BadServerResponse("Unsupported room version.")),
}
create_event_content.insert(
"room_version".into(),
json!(&body.new_version).try_into().map_err(|_| {
Error::BadRequest(
ErrorKind::BadJson,
"Error forming creation event",
)
})?,
);
create_event_content.insert(
"predecessor".into(),
json!(predecessor).try_into().map_err(|_| {
Error::BadRequest(
ErrorKind::BadJson,
"Error forming creation event",
)
})?,
);
// Validate creation event content
let de_result = serde_json::from_str::<CanonicalJsonObject>(
to_raw_value(&create_event_content)
.expect("Error forming creation event")
.get(),
);
if de_result.is_err() {
return Err(Error::BadRequest(
ErrorKind::BadJson,
"Error forming creation event",
));
}
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_event_content)
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&replacement_room_token,
)
.await?;
// Join the new room
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: services().users.displayname(sender_user)?,
avatar_url: services().users.avatar_url(sender_user)?,
is_direct: None,
third_party_invite: None,
blurhash: services().users.blurhash(sender_user)?,
reason: None,
join_authorized_via_users_server: None,
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.to_string()),
redacts: None,
},
sender_user,
&replacement_room_token,
)
.await?;
// Recommended transferable state events list from the specs
let transferable_state_events = vec![
StateEventType::RoomServerAcl,
StateEventType::RoomEncryption,
StateEventType::RoomName,
StateEventType::RoomAvatar,
StateEventType::RoomTopic,
StateEventType::RoomGuestAccess,
StateEventType::RoomHistoryVisibility,
StateEventType::RoomJoinRules,
StateEventType::RoomPowerLevels,
];
// Replicate transferable state events to the new room
for event_type in transferable_state_events {
let event_content = match services()
.rooms
.state_accessor
.room_state_get(&body.room_id, &event_type, "")?
{
Some(v) => v.content.clone(),
// Skipping missing events.
None => continue,
};
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: event_type.to_string().into(),
content: event_content,
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&replacement_room_token,
)
.await?;
}
// Moves any local aliases to the new room
for alias in services()
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.filter_map(Result::ok)
{
services().rooms.alias.set_alias(
&alias,
&replacement_room,
sender_user,
)?;
}
// Get the old room power levels
let mut power_levels_event_content: RoomPowerLevelsEventContent =
serde_json::from_str(
services()
.rooms
.state_accessor
.room_state_get(
&body.room_id,
&StateEventType::RoomPowerLevels,
"",
)?
.ok_or_else(|| {
Error::bad_database(
"Found room without m.room.create event.",
)
})?
.content
.get(),
)
.map_err(|_| Error::bad_database("Invalid room event in database."))?;
// Setting events_default and invite to the greater of 50 and users_default
// + 1
let new_level =
max(int!(50), power_levels_event_content.users_default + int!(1));
power_levels_event_content.events_default = new_level;
power_levels_event_content.invite = new_level;
// Modify the power levels in the old room to prevent sending of events and
// inviting new users
let _ = services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_event_content)
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&original_room_token,
)
.await?;
// Return the replacement room id
Ok(Ra(upgrade_room::v3::Response {
replacement_room,
}))
}