Skip to content

Commit 8e0dba6

Browse files
pixlwavestefanceriu
authored andcommitted
spaces: Add methods to add/remove space children.
1 parent 2f7d2b3 commit 8e0dba6

File tree

3 files changed

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

3 files changed

+304
-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: 234 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};
@@ -59,10 +59,22 @@ pub mod room_list;
5959
/// Possible [`SpaceService`] errors.
6060
#[derive(Debug, Error)]
6161
pub enum Error {
62+
/// The user ID was not available from the client.
63+
#[error("User ID not available from client")]
64+
UserIdNotFound,
65+
6266
/// The requested room was not found.
6367
#[error("Room `{0}` not found")]
6468
RoomNotFound(OwnedRoomId),
6569

70+
/// The space parent/child state was missing.
71+
#[error("Missing `{0}` for `{1}`")]
72+
MissingState(StateEventType, OwnedRoomId),
73+
74+
/// Failed to set the expected m.space.parent or m.space.child state events.
75+
#[error("Failed updating space parent/child relationship")]
76+
UpdateRelationship(SDKError),
77+
6678
/// Failed to leave a space.
6779
#[error("Failed to leave space")]
6880
LeaveSpace(SDKError),
@@ -204,6 +216,88 @@ impl SpaceService {
204216
SpaceRoomList::new(self.client.clone(), space_id).await
205217
}
206218

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

338432
#[cfg(test)]
339433
mod tests {
434+
use std::collections::BTreeMap;
435+
340436
use assert_matches2::assert_let;
341437
use eyeball_im::VectorDiff;
342438
use futures_util::{StreamExt, pin_mut};
@@ -345,7 +441,7 @@ mod tests {
345441
JoinedRoomBuilder, LeftRoomBuilder, RoomAccountDataTestEvent, async_test,
346442
event_factory::EventFactory,
347443
};
348-
use ruma::{RoomVersionId, UserId, owned_room_id, room_id};
444+
use ruma::{RoomVersionId, UserId, event_id, owned_room_id, room_id, user_id};
349445
use serde_json::json;
350446
use stream_assert::{assert_next_eq, assert_pending};
351447

@@ -620,6 +716,118 @@ mod tests {
620716
);
621717
}
622718

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

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)