From f36757027eacc27f47f6415d998be6cf61cc4f0a Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Wed, 6 Nov 2024 18:27:40 +0000 Subject: [PATCH] split api/client/room Signed-off-by: Jason Volk --- src/api/client/room/aliases.rs | 40 +++ src/api/client/{room.rs => room/create.rs} | 359 +-------------------- src/api/client/room/event.rs | 38 +++ src/api/client/room/mod.rs | 9 + src/api/client/room/upgrade.rs | 294 +++++++++++++++++ 5 files changed, 388 insertions(+), 352 deletions(-) create mode 100644 src/api/client/room/aliases.rs rename src/api/client/{room.rs => room/create.rs} (65%) create mode 100644 src/api/client/room/event.rs create mode 100644 src/api/client/room/mod.rs create mode 100644 src/api/client/room/upgrade.rs diff --git a/src/api/client/room/aliases.rs b/src/api/client/room/aliases.rs new file mode 100644 index 00000000..e530b260 --- /dev/null +++ b/src/api/client/room/aliases.rs @@ -0,0 +1,40 @@ +use axum::extract::State; +use conduit::{Error, Result}; +use futures::StreamExt; +use ruma::api::client::{error::ErrorKind, room::aliases}; + +use crate::Ruma; + +/// # `GET /_matrix/client/r0/rooms/{roomId}/aliases` +/// +/// Lists all aliases of the room. +/// +/// - Only users joined to the room are allowed to call this, or if +/// `history_visibility` is world readable in the room +pub(crate) async fn get_room_aliases_route( + State(services): State, body: Ruma, +) -> Result { + let sender_user = body.sender_user.as_ref().expect("user is authenticated"); + + if !services + .rooms + .state_accessor + .user_can_see_state_events(sender_user, &body.room_id) + .await + { + return Err(Error::BadRequest( + ErrorKind::forbidden(), + "You don't have permission to view this room.", + )); + } + + Ok(aliases::v3::Response { + aliases: services + .rooms + .alias + .local_aliases_for_room(&body.room_id) + .map(ToOwned::to_owned) + .collect() + .await, + }) +} diff --git a/src/api/client/room.rs b/src/api/client/room/create.rs similarity index 65% rename from src/api/client/room.rs rename to src/api/client/room/create.rs index b6683ef4..2ccb1c87 100644 --- a/src/api/client/room.rs +++ b/src/api/client/room/create.rs @@ -1,12 +1,12 @@ -use std::{cmp::max, collections::BTreeMap}; +use std::collections::BTreeMap; use axum::extract::State; -use conduit::{debug_info, debug_warn, err, Err}; -use futures::{FutureExt, StreamExt, TryFutureExt}; +use conduit::{debug_info, debug_warn, error, info, pdu::PduBuilder, warn, Err, Error, Result}; +use futures::FutureExt; use ruma::{ api::client::{ error::ErrorKind, - room::{self, aliases, create_room, get_room_event, upgrade_room}, + room::{self, create_room}, }, events::{ room::{ @@ -18,36 +18,18 @@ use ruma::{ member::{MembershipState, RoomMemberEventContent}, name::RoomNameEventContent, power_levels::RoomPowerLevelsEventContent, - tombstone::RoomTombstoneEventContent, topic::RoomTopicEventContent, }, - StateEventType, TimelineEventType, + TimelineEventType, }, int, serde::{JsonObject, Raw}, CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, }; use serde_json::{json, value::to_raw_value}; -use tracing::{error, info, warn}; +use service::{appservice::RegistrationInfo, Services}; -use super::invite_helper; -use crate::{ - service::{appservice::RegistrationInfo, pdu::PduBuilder, Services}, - Error, Result, Ruma, -}; - -/// Recommended transferable state events list from the spec -const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 9] = &[ - StateEventType::RoomServerAcl, - StateEventType::RoomEncryption, - StateEventType::RoomName, - StateEventType::RoomAvatar, - StateEventType::RoomTopic, - StateEventType::RoomGuestAccess, - StateEventType::RoomHistoryVisibility, - StateEventType::RoomJoinRules, - StateEventType::RoomPowerLevels, -]; +use crate::{client::invite_helper, Ruma}; /// # `POST /_matrix/client/v3/createRoom` /// @@ -479,333 +461,6 @@ pub(crate) async fn create_room_route( Ok(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( - State(services): State, ref body: Ruma, -) -> Result { - Ok(get_room_event::v3::Response { - event: services - .rooms - .timeline - .get_pdu_owned(&body.event_id) - .map_err(|_| err!(Request(NotFound("Event {} not found.", &body.event_id)))) - .and_then(|event| async move { - services - .rooms - .state_accessor - .user_can_see_event(body.sender_user(), &event.room_id, &body.event_id) - .await - .then_some(event) - .ok_or_else(|| err!(Request(Forbidden("You don't have permission to view this event.")))) - }) - .map_ok(|mut event| { - event.add_age().ok(); - event.to_room_event() - }) - .await?, - }) -} - -/// # `GET /_matrix/client/r0/rooms/{roomId}/aliases` -/// -/// Lists all aliases of the room. -/// -/// - Only users joined to the room are allowed to call this, or if -/// `history_visibility` is world readable in the room -pub(crate) async fn get_room_aliases_route( - State(services): State, body: Ruma, -) -> Result { - let sender_user = body.sender_user.as_ref().expect("user is authenticated"); - - if !services - .rooms - .state_accessor - .user_can_see_state_events(sender_user, &body.room_id) - .await - { - return Err(Error::BadRequest( - ErrorKind::forbidden(), - "You don't have permission to view this room.", - )); - } - - Ok(aliases::v3::Response { - aliases: services - .rooms - .alias - .local_aliases_for_room(&body.room_id) - .map(ToOwned::to_owned) - .collect() - .await, - }) -} - -/// # `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 -pub(crate) async fn upgrade_room_route( - State(services): State, body: Ruma, -) -> Result { - 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()); - - let _short_id = services - .rooms - .short - .get_or_create_shortroomid(&replacement_room) - .await; - - let state_lock = services.rooms.state.mutex.lock(&body.room_id).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::state( - String::new(), - &RoomTombstoneEventContent { - body: "This room has been replaced".to_owned(), - replacement_room: replacement_room.clone(), - }, - ), - sender_user, - &body.room_id, - &state_lock, - ) - .await?; - - // Change lock to replacement room - drop(state_lock); - let state_lock = services.rooms.state.mutex.lock(&replacement_room).await; - - // Get the old room creation event - let mut create_event_content: CanonicalJsonObject = services - .rooms - .state_accessor - .room_state_get_content(&body.room_id, &StateEventType::RoomCreate, "") - .await - .map_err(|_| err!(Database("Found room without m.room.create event.")))?; - - // 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 - { - use RoomVersionId::*; - match body.new_version { - V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => { - create_event_content.insert( - "creator".into(), - json!(&sender_user).try_into().map_err(|e| { - info!("Error forming creation event: {e}"); - Error::BadRequest(ErrorKind::BadJson, "Error forming creation event") - })?, - ); - }, - _ => { - // "creator" key no longer exists in V11+ rooms - 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 - if serde_json::from_str::( - to_raw_value(&create_event_content) - .expect("Error forming creation event") - .get(), - ) - .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, - timestamp: None, - }, - sender_user, - &replacement_room, - &state_lock, - ) - .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).await.ok(), - avatar_url: services.users.avatar_url(sender_user).await.ok(), - is_direct: None, - third_party_invite: None, - blurhash: services.users.blurhash(sender_user).await.ok(), - 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, - timestamp: None, - }, - sender_user, - &replacement_room, - &state_lock, - ) - .await?; - - // 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, "") - .await - { - Ok(v) => v.content.clone(), - Err(_) => continue, // Skipping missing events. - }; - - services - .rooms - .timeline - .build_and_append_pdu( - PduBuilder { - event_type: event_type.to_string().into(), - content: event_content, - state_key: Some(String::new()), - ..Default::default() - }, - sender_user, - &replacement_room, - &state_lock, - ) - .await?; - } - - // Moves any local aliases to the new room - let mut local_aliases = services - .rooms - .alias - .local_aliases_for_room(&body.room_id) - .boxed(); - - while let Some(alias) = local_aliases.next().await { - services - .rooms - .alias - .remove_alias(alias, sender_user) - .await?; - - services - .rooms - .alias - .set_alias(alias, &replacement_room, sender_user)?; - } - - // Get the old room power levels - let power_levels_event_content: RoomPowerLevelsEventContent = services - .rooms - .state_accessor - .room_state_get_content(&body.room_id, &StateEventType::RoomPowerLevels, "") - .await - .map_err(|_| err!(Database("Found room without m.room.power_levels event.")))?; - - // 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 - .checked_add(int!(1)) - .ok_or_else(|| err!(Request(BadJson("users_default power levels event content is not valid"))))?, - ); - - // Modify the power levels in the old room to prevent sending of events and - // inviting new users - services - .rooms - .timeline - .build_and_append_pdu( - PduBuilder::state( - String::new(), - &RoomPowerLevelsEventContent { - events_default: new_level, - invite: new_level, - ..power_levels_event_content - }, - ), - sender_user, - &body.room_id, - &state_lock, - ) - .await?; - - drop(state_lock); - - // Return the replacement room id - Ok(upgrade_room::v3::Response { - replacement_room, - }) -} - /// creates the power_levels_content for the PDU builder fn default_power_levels_content( power_level_content_override: Option<&Raw>, visibility: &room::Visibility, diff --git a/src/api/client/room/event.rs b/src/api/client/room/event.rs new file mode 100644 index 00000000..0f44f25d --- /dev/null +++ b/src/api/client/room/event.rs @@ -0,0 +1,38 @@ +use axum::extract::State; +use conduit::{err, Result}; +use futures::TryFutureExt; +use ruma::api::client::room::get_room_event; + +use crate::Ruma; + +/// # `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( + State(services): State, ref body: Ruma, +) -> Result { + Ok(get_room_event::v3::Response { + event: services + .rooms + .timeline + .get_pdu_owned(&body.event_id) + .map_err(|_| err!(Request(NotFound("Event {} not found.", &body.event_id)))) + .and_then(|event| async move { + services + .rooms + .state_accessor + .user_can_see_event(body.sender_user(), &event.room_id, &body.event_id) + .await + .then_some(event) + .ok_or_else(|| err!(Request(Forbidden("You don't have permission to view this event.")))) + }) + .map_ok(|mut event| { + event.add_age().ok(); + event.to_room_event() + }) + .await?, + }) +} diff --git a/src/api/client/room/mod.rs b/src/api/client/room/mod.rs new file mode 100644 index 00000000..fa2d168f --- /dev/null +++ b/src/api/client/room/mod.rs @@ -0,0 +1,9 @@ +mod aliases; +mod create; +mod event; +mod upgrade; + +pub(crate) use self::{ + aliases::get_room_aliases_route, create::create_room_route, event::get_room_event_route, + upgrade::upgrade_room_route, +}; diff --git a/src/api/client/room/upgrade.rs b/src/api/client/room/upgrade.rs new file mode 100644 index 00000000..ad5c356e --- /dev/null +++ b/src/api/client/room/upgrade.rs @@ -0,0 +1,294 @@ +use std::cmp::max; + +use axum::extract::State; +use conduit::{err, info, pdu::PduBuilder, Error, Result}; +use futures::StreamExt; +use ruma::{ + api::client::{error::ErrorKind, room::upgrade_room}, + events::{ + room::{ + member::{MembershipState, RoomMemberEventContent}, + power_levels::RoomPowerLevelsEventContent, + tombstone::RoomTombstoneEventContent, + }, + StateEventType, TimelineEventType, + }, + int, CanonicalJsonObject, RoomId, RoomVersionId, +}; +use serde_json::{json, value::to_raw_value}; + +use crate::Ruma; + +/// Recommended transferable state events list from the spec +const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 9] = &[ + StateEventType::RoomServerAcl, + StateEventType::RoomEncryption, + StateEventType::RoomName, + StateEventType::RoomAvatar, + StateEventType::RoomTopic, + StateEventType::RoomGuestAccess, + StateEventType::RoomHistoryVisibility, + StateEventType::RoomJoinRules, + StateEventType::RoomPowerLevels, +]; + +/// # `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 +pub(crate) async fn upgrade_room_route( + State(services): State, body: Ruma, +) -> Result { + 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()); + + let _short_id = services + .rooms + .short + .get_or_create_shortroomid(&replacement_room) + .await; + + let state_lock = services.rooms.state.mutex.lock(&body.room_id).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::state( + String::new(), + &RoomTombstoneEventContent { + body: "This room has been replaced".to_owned(), + replacement_room: replacement_room.clone(), + }, + ), + sender_user, + &body.room_id, + &state_lock, + ) + .await?; + + // Change lock to replacement room + drop(state_lock); + let state_lock = services.rooms.state.mutex.lock(&replacement_room).await; + + // Get the old room creation event + let mut create_event_content: CanonicalJsonObject = services + .rooms + .state_accessor + .room_state_get_content(&body.room_id, &StateEventType::RoomCreate, "") + .await + .map_err(|_| err!(Database("Found room without m.room.create event.")))?; + + // 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 + { + use RoomVersionId::*; + match body.new_version { + V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => { + create_event_content.insert( + "creator".into(), + json!(&sender_user).try_into().map_err(|e| { + info!("Error forming creation event: {e}"); + Error::BadRequest(ErrorKind::BadJson, "Error forming creation event") + })?, + ); + }, + _ => { + // "creator" key no longer exists in V11+ rooms + 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 + if serde_json::from_str::( + to_raw_value(&create_event_content) + .expect("Error forming creation event") + .get(), + ) + .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, + timestamp: None, + }, + sender_user, + &replacement_room, + &state_lock, + ) + .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).await.ok(), + avatar_url: services.users.avatar_url(sender_user).await.ok(), + is_direct: None, + third_party_invite: None, + blurhash: services.users.blurhash(sender_user).await.ok(), + 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, + timestamp: None, + }, + sender_user, + &replacement_room, + &state_lock, + ) + .await?; + + // 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, "") + .await + { + Ok(v) => v.content.clone(), + Err(_) => continue, // Skipping missing events. + }; + + services + .rooms + .timeline + .build_and_append_pdu( + PduBuilder { + event_type: event_type.to_string().into(), + content: event_content, + state_key: Some(String::new()), + ..Default::default() + }, + sender_user, + &replacement_room, + &state_lock, + ) + .await?; + } + + // Moves any local aliases to the new room + let mut local_aliases = services + .rooms + .alias + .local_aliases_for_room(&body.room_id) + .boxed(); + + while let Some(alias) = local_aliases.next().await { + services + .rooms + .alias + .remove_alias(alias, sender_user) + .await?; + + services + .rooms + .alias + .set_alias(alias, &replacement_room, sender_user)?; + } + + // Get the old room power levels + let power_levels_event_content: RoomPowerLevelsEventContent = services + .rooms + .state_accessor + .room_state_get_content(&body.room_id, &StateEventType::RoomPowerLevels, "") + .await + .map_err(|_| err!(Database("Found room without m.room.power_levels event.")))?; + + // 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 + .checked_add(int!(1)) + .ok_or_else(|| err!(Request(BadJson("users_default power levels event content is not valid"))))?, + ); + + // Modify the power levels in the old room to prevent sending of events and + // inviting new users + services + .rooms + .timeline + .build_and_append_pdu( + PduBuilder::state( + String::new(), + &RoomPowerLevelsEventContent { + events_default: new_level, + invite: new_level, + ..power_levels_event_content + }, + ), + sender_user, + &body.room_id, + &state_lock, + ) + .await?; + + drop(state_lock); + + // Return the replacement room id + Ok(upgrade_room::v3::Response { + replacement_room, + }) +}