@@ -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 } ;
@@ -59,10 +59,22 @@ pub mod room_list;
5959/// Possible [`SpaceService`] errors.
6060#[ derive( Debug , Error ) ]
6161pub 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) ]
339433mod 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}
0 commit comments