grapevine/src/api/client_server/room.rs
Olivia Lee a34bca3986
transfer local canonical aliases to new room on upgrade
When upgrading rooms, we reassign any local aliases from the old room to
the new room. This commit updates the m.room.canonical_alias events in
the old and new rooms to reflect which aliases were moved. The spec is
unclear on whether the server should do this[1], but it's consistent
with synapse's behavior.

I went with putting the canonical alias update logic inline, rather than
something like add_canonical_alias and remove_canonical_alias helper
functions to the alias service, because it's desirable to have the alias
updates be sent as a single event than a separate event for each change.

[1]: https://github.com/matrix-org/matrix-spec/issues/2142
2025-05-18 14:13:41 -07:00

919 lines
29 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,
};
use serde_json::{json, value::to_raw_value};
use tracing::{info, warn};
use crate::{
api::client_server::invite_helper, service::pdu::PduBuilder, services,
utils::room_version::RoomVersion, 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_id = 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 room_version = RoomVersion::try_from(&room_version_id)?;
let content = if let Some(content) = &body.creation_content {
let mut content = content
.deserialize_as::<CanonicalJsonObject>()
.expect("Invalid creation content");
if room_version.create_event_creator_prop {
content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|_| {
Error::BadRequest(
ErrorKind::BadJson,
"Invalid creation content",
)
})?,
);
}
content.insert(
"room_version".into(),
json!(room_version_id.as_str()).try_into().map_err(|_| {
Error::BadRequest(
ErrorKind::BadJson,
"Invalid creation content",
)
})?,
);
content
} else {
let content = if room_version.create_event_creator_prop {
RoomCreateEventContent::new_v1(sender_user.to_owned())
} else {
RoomCreateEventContent::new_v11()
};
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_id.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.",
));
}
let new_version = RoomVersion::try_from(&body.new_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
if new_version.create_event_creator_prop {
create_event_content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|_| {
Error::BadRequest(
ErrorKind::BadJson,
"Error forming creation event",
)
})?,
);
} else {
create_event_content.remove("creator");
}
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
let mut old_canonical_alias: RoomCanonicalAliasEventContent = services()
.rooms
.state_accessor
.room_state_get(&body.room_id, &StateEventType::RoomCanonicalAlias, "")?
.and_then(|event| {
serde_json::from_str(event.content.get())
.inspect_err(|_| {
Error::bad_database("invalid m.room.canonical_alias event");
})
.ok()
})
.unwrap_or_default();
let mut new_canonical_alias = RoomCanonicalAliasEventContent::default();
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,
)?;
if old_canonical_alias.alias.as_ref() == Some(&alias) {
new_canonical_alias.alias = Some(alias.clone());
old_canonical_alias.alias = None;
}
let mut is_alt_alias = false;
old_canonical_alias.alt_aliases.retain(|a| {
let equal = a == &alias;
is_alt_alias |= equal;
!equal
});
if is_alt_alias {
new_canonical_alias.alt_aliases.push(alias);
}
}
// Send updated events to both rooms if we moved any canonical aliases
let new_canonical_alias_empty = new_canonical_alias.alias.is_none()
&& new_canonical_alias.alt_aliases.is_empty();
if !new_canonical_alias_empty {
if let Err(error) = services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCanonicalAlias,
content: to_raw_value(&old_canonical_alias)
.expect("event is valid"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&original_room_token,
)
.await
{
// The sender may not have had permission to send
// m.room.canonical_alias in the old room. Don't treat
// this as fatal.
warn!(
%error,
"Failed to remove local canonical aliases from old room"
);
}
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCanonicalAlias,
content: to_raw_value(&new_canonical_alias)
.expect("event is valid"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
},
sender_user,
&replacement_room_token,
)
.await?;
}
// 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,
}))
}