@@ -40,13 +40,13 @@ use matrix_sdk_common::executor::spawn;
4040use ruma:: {
4141 OwnedRoomId , RoomId ,
4242 events:: {
43- self , SyncStateEvent ,
43+ self , StateEventType , SyncStateEvent ,
4444 space:: { child:: SpaceChildEventContent , parent:: SpaceParentEventContent } ,
4545 } ,
4646} ;
4747use thiserror:: Error ;
4848use tokio:: sync:: Mutex as AsyncMutex ;
49- use tracing:: error;
49+ use tracing:: { error, warn } ;
5050
5151use crate :: spaces:: { graph:: SpaceGraph , leave:: LeaveSpaceHandle } ;
5252pub 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) ]
339425mod 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}
0 commit comments