Skip to content

Commit e4c0fab

Browse files
committed
spaces: Add methods to add/remove space children.
1 parent 9ab886f commit e4c0fab

File tree

3 files changed

+296
-3
lines changed
  • bindings/matrix-sdk-ffi/src
  • crates
    • matrix-sdk-ui/src/spaces
    • matrix-sdk/src/test_utils/mocks

3 files changed

+296
-3
lines changed

bindings/matrix-sdk-ffi/src/spaces.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,28 @@ impl SpaceService {
8787
Ok(Arc::new(SpaceRoomList::new(self.inner.space_room_list(space_id).await)))
8888
}
8989

90+
pub async fn add_child_to_space(
91+
&self,
92+
child_id: String,
93+
space_id: String,
94+
) -> Result<(), ClientError> {
95+
let space_id = RoomId::parse(space_id)?;
96+
let child_id = RoomId::parse(child_id)?;
97+
98+
self.inner.add_child_to_space(child_id, space_id).await.map_err(ClientError::from)
99+
}
100+
101+
pub async fn remove_child_from_space(
102+
&self,
103+
child_id: String,
104+
space_id: String,
105+
) -> Result<(), ClientError> {
106+
let space_id = RoomId::parse(space_id)?;
107+
let child_id = RoomId::parse(child_id)?;
108+
109+
self.inner.remove_child_from_space(child_id, space_id).await.map_err(ClientError::from)
110+
}
111+
90112
/// Start a space leave process returning a [`LeaveSpaceHandle`] from which
91113
/// rooms can be retrieved in reversed BFS order starting from the requested
92114
/// `space_id` graph node. If the room is unknown then an error will be

crates/matrix-sdk-ui/src/spaces/mod.rs

Lines changed: 226 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ use matrix_sdk_common::executor::spawn;
4040
use ruma::{
4141
OwnedRoomId, RoomId,
4242
events::{
43-
self, SyncStateEvent,
43+
self, StateEventType, SyncStateEvent,
4444
space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
4545
},
4646
};
4747
use thiserror::Error;
4848
use tokio::sync::Mutex as AsyncMutex;
49-
use tracing::error;
49+
use tracing::{error, warn};
5050

5151
use crate::spaces::{graph::SpaceGraph, leave::LeaveSpaceHandle};
5252
pub use crate::spaces::{room::SpaceRoom, room_list::SpaceRoomList};
@@ -63,6 +63,14 @@ pub enum Error {
6363
#[error("Room `{0}` not found")]
6464
RoomNotFound(OwnedRoomId),
6565

66+
/// The space parent/child state was missing.
67+
#[error("Missing `{0}` for `{1}`")]
68+
MissingState(StateEventType, OwnedRoomId),
69+
70+
/// Failed to set the expected m.space.parent or m.space.child state events.
71+
#[error("Failed updating space parent/child relationship")]
72+
UpdateRelationship(SDKError),
73+
6674
/// Failed to leave a space.
6775
#[error("Failed to leave space")]
6876
LeaveSpace(SDKError),
@@ -204,6 +212,84 @@ impl SpaceService {
204212
SpaceRoomList::new(self.client.clone(), space_id).await
205213
}
206214

215+
pub async fn add_child_to_space(
216+
&self,
217+
child_id: OwnedRoomId,
218+
space_id: OwnedRoomId,
219+
) -> Result<(), Error> {
220+
let space_room =
221+
self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
222+
let child_room =
223+
self.client.get_room(&child_id).ok_or(Error::RoomNotFound(child_id.to_owned()))?;
224+
225+
// Add the child to the space.
226+
let child_route = child_room.route().await.map_err(Error::UpdateRelationship)?;
227+
space_room
228+
.send_state_event_for_key(&child_id, SpaceChildEventContent::new(child_route))
229+
.await
230+
.map_err(Error::UpdateRelationship)?;
231+
232+
// Add the space as parent of the child if allowed.
233+
if let Some(user_id) = self.client.user_id()
234+
&& let Ok(power_levels) = child_room.power_levels().await
235+
&& power_levels.user_can_send_state(user_id, StateEventType::SpaceParent)
236+
{
237+
let parent_route = space_room.route().await.map_err(Error::UpdateRelationship)?;
238+
child_room
239+
.send_state_event_for_key(&space_id, SpaceParentEventContent::new(parent_route))
240+
.await
241+
.map_err(Error::UpdateRelationship)?;
242+
} else {
243+
warn!("The current user doesn't have permission to set the child's parent.");
244+
}
245+
246+
Ok(())
247+
}
248+
249+
pub async fn remove_child_from_space(
250+
&self,
251+
child_id: OwnedRoomId,
252+
space_id: OwnedRoomId,
253+
) -> Result<(), Error> {
254+
let space_room =
255+
self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
256+
let child_room =
257+
self.client.get_room(&child_id).ok_or(Error::RoomNotFound(child_id.to_owned()))?;
258+
259+
if space_room
260+
.get_state_event_static_for_key::<SpaceChildEventContent, _>(&child_id)
261+
.await
262+
.is_err()
263+
{
264+
return Err(Error::MissingState(StateEventType::SpaceChild, child_id));
265+
}
266+
// Redacting state is a "weird" thing to do, so send {} instead.
267+
//
268+
// Specifically, "The redaction of the state doesn't participate in state
269+
// resolution so behaves quite differently from e.g. sending an empty form of
270+
// that state events".
271+
space_room
272+
.send_state_event_raw("m.space.child", child_id.as_str(), serde_json::json!({}))
273+
.await
274+
.map_err(Error::UpdateRelationship)?;
275+
276+
if child_room
277+
.get_state_event_static_for_key::<SpaceParentEventContent, _>(&space_id)
278+
.await
279+
.is_err()
280+
{
281+
warn!("A space parent event wasn't found on the child room, ignoring.");
282+
return Ok(());
283+
}
284+
// Same as the comment above.
285+
child_room
286+
.send_state_event_raw("m.space.parent", space_id.as_str(), serde_json::json!({}))
287+
.await
288+
.map_err(Error::UpdateRelationship)?;
289+
290+
Ok(())
291+
}
292+
207293
/// Start a space leave process returning a [`LeaveSpaceHandle`] from which
208294
/// rooms can be retrieved in reversed BFS order starting from the requested
209295
/// `space_id` graph node. If the room is unknown then an error will be
@@ -337,6 +423,8 @@ impl SpaceService {
337423

338424
#[cfg(test)]
339425
mod tests {
426+
use std::collections::BTreeMap;
427+
340428
use assert_matches2::assert_let;
341429
use eyeball_im::VectorDiff;
342430
use futures_util::{StreamExt, pin_mut};
@@ -345,7 +433,7 @@ mod tests {
345433
JoinedRoomBuilder, LeftRoomBuilder, RoomAccountDataTestEvent, async_test,
346434
event_factory::EventFactory,
347435
};
348-
use ruma::{RoomVersionId, UserId, owned_room_id, room_id};
436+
use ruma::{RoomVersionId, UserId, event_id, owned_room_id, room_id, user_id};
349437
use serde_json::json;
350438
use stream_assert::{assert_next_eq, assert_pending};
351439

@@ -620,6 +708,118 @@ mod tests {
620708
);
621709
}
622710

711+
#[async_test]
712+
async fn test_add_child_to_space() {
713+
// Given a space and child room where the user is admin of both.
714+
let server = MatrixMockServer::new().await;
715+
let client = server.client_builder().build().await;
716+
let user_id = client.user_id().unwrap();
717+
let factory = EventFactory::new();
718+
719+
server.mock_room_state_encryption().plain().mount().await;
720+
721+
let space_child_event_id = event_id!("$1");
722+
let space_parent_event_id = event_id!("$2");
723+
server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
724+
server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
725+
726+
let space_id = room_id!("!my_space:example.org");
727+
let child_id = room_id!("!my_child:example.org");
728+
729+
add_rooms_with_power_level(
730+
vec![(space_id, 100), (child_id, 100)],
731+
&client,
732+
&server,
733+
&factory,
734+
user_id,
735+
)
736+
.await;
737+
738+
let space_service = SpaceService::new(client.clone());
739+
740+
// When adding the child to the space.
741+
let result =
742+
space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
743+
744+
// Then both space child and parent events are set successfully.
745+
assert!(result.is_ok());
746+
}
747+
748+
#[async_test]
749+
async fn test_add_child_to_space_without_space_admin() {
750+
// Given a space and child room where the user is a regular member of both.
751+
let server = MatrixMockServer::new().await;
752+
let client = server.client_builder().build().await;
753+
let user_id = client.user_id().unwrap();
754+
let factory = EventFactory::new();
755+
756+
server.mock_room_state_encryption().plain().mount().await;
757+
758+
server.mock_set_space_child().unauthorized().expect(1).mount().await;
759+
server.mock_set_space_parent().unauthorized().expect(0).mount().await;
760+
761+
let space_id = room_id!("!my_space:example.org");
762+
let child_id = room_id!("!my_child:example.org");
763+
764+
add_rooms_with_power_level(
765+
vec![(space_id, 0), (child_id, 0)],
766+
&client,
767+
&server,
768+
&factory,
769+
user_id,
770+
)
771+
.await;
772+
773+
let space_service = SpaceService::new(client.clone());
774+
775+
// When adding the child to the space.
776+
let result =
777+
space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
778+
779+
// Then the operation fails when trying to set the space child event and the
780+
// parent event is not attempted.
781+
assert!(result.is_err());
782+
}
783+
784+
#[async_test]
785+
async fn test_add_child_to_space_without_child_admin() {
786+
// Given a space and child room where the user is admin of the space but not of
787+
// the child.
788+
let server = MatrixMockServer::new().await;
789+
let client = server.client_builder().build().await;
790+
let user_id = client.user_id().unwrap();
791+
let factory = EventFactory::new();
792+
793+
server.mock_room_state_encryption().plain().mount().await;
794+
795+
let space_child_event_id = event_id!("$1");
796+
server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
797+
server.mock_set_space_parent().unauthorized().expect(0).mount().await;
798+
799+
let space_id = room_id!("!my_space:example.org");
800+
let child_id = room_id!("!my_child:example.org");
801+
802+
add_rooms_with_power_level(
803+
vec![(space_id, 100), (child_id, 0)],
804+
&client,
805+
&server,
806+
&factory,
807+
user_id,
808+
)
809+
.await;
810+
811+
let space_service = SpaceService::new(client.clone());
812+
813+
// When adding the child to the space.
814+
let result =
815+
space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
816+
817+
error!("result: {:?}", result);
818+
// Then the operation succeeds in setting the space child event and the parent
819+
// event is not attempted.
820+
assert!(result.is_ok());
821+
}
822+
623823
async fn add_space_rooms_with(
624824
rooms: Vec<(&RoomId, Option<&str>)>,
625825
client: &Client,
@@ -643,4 +843,27 @@ mod tests {
643843
server.sync_room(client, builder).await;
644844
}
645845
}
846+
847+
async fn add_rooms_with_power_level(
848+
rooms: Vec<(&RoomId, i32)>,
849+
client: &Client,
850+
server: &MatrixMockServer,
851+
factory: &EventFactory,
852+
user_id: &UserId,
853+
) {
854+
for (room_id, power_level) in rooms {
855+
let mut builder = JoinedRoomBuilder::new(room_id);
856+
let mut power_levels = BTreeMap::from([(user_id.to_owned(), power_level.into())]);
857+
858+
builder = builder
859+
.add_state_event(
860+
factory.create(user_id!("@creator:example.com"), RoomVersionId::V1),
861+
)
862+
.add_state_event(
863+
factory.power_levels(&mut power_levels).state_key("").sender(user_id),
864+
);
865+
866+
server.sync_room(client, builder).await;
867+
}
868+
}
646869
}

crates/matrix-sdk/src/test_utils/mocks/mod.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,20 @@ impl MatrixMockServer {
16301630
self.mock_endpoint(mock, GetHierarchyEndpoint).expect_default_access_token()
16311631
}
16321632

1633+
/// Create a prebuilt mock for the endpoint used to set a space child.
1634+
pub fn mock_set_space_child(&self) -> MockEndpoint<'_, SetSpaceChildEndpoint> {
1635+
let mock = Mock::given(method("PUT"))
1636+
.and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.space.child/.*?"));
1637+
self.mock_endpoint(mock, SetSpaceChildEndpoint).expect_default_access_token()
1638+
}
1639+
1640+
/// Create a prebuilt mock for the endpoint used to set a space parent.
1641+
pub fn mock_set_space_parent(&self) -> MockEndpoint<'_, SetSpaceParentEndpoint> {
1642+
let mock = Mock::given(method("PUT"))
1643+
.and(path_regex(r"^/_matrix/client/v3/rooms/.*/state/m.space.parent"));
1644+
self.mock_endpoint(mock, SetSpaceParentEndpoint).expect_default_access_token()
1645+
}
1646+
16331647
/// Create a prebuilt mock for the endpoint used to get a profile field.
16341648
pub fn mock_get_profile_field(
16351649
&self,
@@ -4680,6 +4694,40 @@ impl<'a> MockEndpoint<'a, GetHierarchyEndpoint> {
46804694
}
46814695
}
46824696

4697+
/// A prebuilt mock for `PUT
4698+
/// /_matrix/client/v3/rooms/{roomId}/state/m.space.child/{stateKey}`
4699+
pub struct SetSpaceChildEndpoint;
4700+
4701+
impl<'a> MockEndpoint<'a, SetSpaceChildEndpoint> {
4702+
/// Returns a successful response with a given event id.
4703+
pub fn ok(self, event_id: OwnedEventId) -> MatrixMock<'a> {
4704+
self.ok_with_event_id(event_id)
4705+
}
4706+
4707+
/// Returns an error response with a generic error code indicating the
4708+
/// client is not authorized to set space children.
4709+
pub fn unauthorized(self) -> MatrixMock<'a> {
4710+
self.respond_with(ResponseTemplate::new(400))
4711+
}
4712+
}
4713+
4714+
/// A prebuilt mock for `PUT
4715+
/// /_matrix/client/v3/rooms/{roomId}/state/m.space.parent/{stateKey}`
4716+
pub struct SetSpaceParentEndpoint;
4717+
4718+
impl<'a> MockEndpoint<'a, SetSpaceParentEndpoint> {
4719+
/// Returns a successful response with a given event id.
4720+
pub fn ok(self, event_id: OwnedEventId) -> MatrixMock<'a> {
4721+
self.ok_with_event_id(event_id)
4722+
}
4723+
4724+
/// Returns an error response with a generic error code indicating the
4725+
/// client is not authorized to set space parents.
4726+
pub fn unauthorized(self) -> MatrixMock<'a> {
4727+
self.respond_with(ResponseTemplate::new(400))
4728+
}
4729+
}
4730+
46834731
/// A prebuilt mock for running simplified sliding sync.
46844732
pub struct SlidingSyncEndpoint;
46854733

0 commit comments

Comments
 (0)