matrix_sdk_ui/spaces/
mod.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for that specific language governing permissions and
13// limitations under the License.
14
15//! High level interfaces for working with Spaces
16//!
17//! The `SpaceService` is an UI oriented, high-level interface for working with
18//! [Matrix Spaces](https://spec.matrix.org/latest/client-server-api/#spaces).
19//! It provides methods to retrieve joined spaces, subscribe
20//! to updates, and navigate space hierarchies.
21//!
22//! It consists of 3 main components:
23//! - `SpaceService`: The main service for managing spaces. It
24//! - `SpaceGraph`: An utility that maps the `m.space.parent` and
25//!   `m.space.child` fields into a graph structure, removing cycles and
26//!   providing access to top level parents.
27//! - `SpaceRoomList`: A component for retrieving a space's children rooms and
28//!   their details.
29
30use std::{cmp::Ordering, collections::HashMap, sync::Arc};
31
32use eyeball_im::{ObservableVector, VectorSubscriberBatchedStream};
33use futures_util::pin_mut;
34use imbl::Vector;
35use itertools::Itertools;
36use matrix_sdk::{
37    Client, Error as SDKError, deserialized_responses::SyncOrStrippedState, executor::AbortOnDrop,
38};
39use matrix_sdk_common::executor::spawn;
40use ruma::{
41    OwnedRoomId, RoomId,
42    events::{
43        self, StateEventType, SyncStateEvent,
44        space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
45    },
46};
47use thiserror::Error;
48use tokio::sync::Mutex as AsyncMutex;
49use tracing::{error, warn};
50
51use crate::spaces::{graph::SpaceGraph, leave::LeaveSpaceHandle};
52pub use crate::spaces::{room::SpaceRoom, room_list::SpaceRoomList};
53
54pub mod graph;
55pub mod leave;
56pub mod room;
57pub mod room_list;
58
59/// Possible [`SpaceService`] errors.
60#[derive(Debug, Error)]
61pub enum Error {
62    /// The user ID was not available from the client.
63    #[error("User ID not available from client")]
64    UserIdNotFound,
65
66    /// The requested room was not found.
67    #[error("Room `{0}` not found")]
68    RoomNotFound(OwnedRoomId),
69
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
78    /// Failed to leave a space.
79    #[error("Failed to leave space")]
80    LeaveSpace(SDKError),
81
82    /// Failed to load members.
83    #[error("Failed to load members")]
84    LoadRoomMembers(SDKError),
85}
86
87struct SpaceState {
88    graph: SpaceGraph,
89    joined_rooms: ObservableVector<SpaceRoom>,
90}
91
92/// The main entry point into the Spaces facilities.
93///
94/// The spaces service is responsible for retrieving one's joined rooms,
95/// building a graph out of their `m.space.parent` and `m.space.child` state
96/// events, and providing access to the top-level spaces and their children.
97///
98/// # Examples
99///
100/// ```no_run
101/// use futures_util::StreamExt;
102/// use matrix_sdk::Client;
103/// use matrix_sdk_ui::spaces::SpaceService;
104/// use ruma::owned_room_id;
105///
106/// # async {
107/// # let client: Client = todo!();
108/// let space_service = SpaceService::new(client.clone());
109///
110/// // Get a list of all the joined spaces
111/// let joined_spaces = space_service.joined_spaces().await;
112///
113/// // And subscribe to changes on them
114/// // `initial_values` is equal to `joined_spaces` if nothing changed meanwhile
115/// let (initial_values, stream) =
116///     space_service.subscribe_to_joined_spaces().await;
117///
118/// while let Some(diffs) = stream.next().await {
119///     println!("Received joined spaces updates: {diffs:?}");
120/// }
121///
122/// // Get a list of all the rooms in a particular space
123/// let room_list = space_service
124///     .space_room_list(owned_room_id!("!some_space:example.org"))
125///     .await;
126///
127/// // Which can be used to retrieve information about the children rooms
128/// let children = room_list.rooms();
129/// # anyhow::Ok(()) };
130/// ```
131pub struct SpaceService {
132    client: Client,
133
134    space_state: Arc<AsyncMutex<SpaceState>>,
135
136    room_update_handle: AsyncMutex<Option<AbortOnDrop<()>>>,
137}
138
139impl SpaceService {
140    /// Creates a new `SpaceService` instance.
141    pub fn new(client: Client) -> Self {
142        Self {
143            client,
144            space_state: Arc::new(AsyncMutex::new(SpaceState {
145                graph: SpaceGraph::new(),
146                joined_rooms: ObservableVector::new(),
147            })),
148            room_update_handle: AsyncMutex::new(None),
149        }
150    }
151
152    /// Subscribes to updates on the joined spaces list. If space rooms are
153    /// joined or left, the stream will yield diffs that reflect the changes.
154    pub async fn subscribe_to_joined_spaces(
155        &self,
156    ) -> (Vector<SpaceRoom>, VectorSubscriberBatchedStream<SpaceRoom>) {
157        let mut room_update_handle = self.room_update_handle.lock().await;
158
159        if room_update_handle.is_none() {
160            let client = self.client.clone();
161            let space_state = Arc::clone(&self.space_state);
162            let all_room_updates_receiver = self.client.subscribe_to_all_room_updates();
163
164            *room_update_handle = Some(AbortOnDrop::new(spawn(async move {
165                pin_mut!(all_room_updates_receiver);
166
167                loop {
168                    match all_room_updates_receiver.recv().await {
169                        Ok(updates) => {
170                            if updates.is_empty() {
171                                continue;
172                            }
173
174                            let (spaces, graph) = Self::joined_spaces_for(&client).await;
175                            Self::update_joined_spaces_if_needed(
176                                Vector::from(spaces),
177                                graph,
178                                &space_state,
179                            )
180                            .await;
181                        }
182                        Err(err) => {
183                            error!("error when listening to room updates: {err}");
184                        }
185                    }
186                }
187            })));
188
189            // Make sure to also update the currently joined spaces for the initial values.
190            let (spaces, graph) = Self::joined_spaces_for(&self.client).await;
191            Self::update_joined_spaces_if_needed(Vector::from(spaces), graph, &self.space_state)
192                .await;
193        }
194
195        self.space_state.lock().await.joined_rooms.subscribe().into_values_and_batched_stream()
196    }
197
198    /// Returns a list of all the top-level joined spaces. It will eagerly
199    /// compute the latest version and also notify subscribers if there were
200    /// any changes.
201    pub async fn joined_spaces(&self) -> Vec<SpaceRoom> {
202        let (spaces, graph) = Self::joined_spaces_for(&self.client).await;
203
204        Self::update_joined_spaces_if_needed(
205            Vector::from(spaces.clone()),
206            graph,
207            &self.space_state,
208        )
209        .await;
210
211        spaces
212    }
213
214    /// Returns a flattened list containing all the spaces where the user has
215    /// permission to send `m.space.child` state events.
216    ///
217    /// Note: Unlike [`Self::joined_spaces()`], this method does not recompute
218    /// graph, nor does it notify subscribers about changes.
219    pub async fn editable_spaces(&self) -> Vec<SpaceRoom> {
220        let Some(user_id) = self.client.user_id() else {
221            return vec![];
222        };
223
224        let graph = &self.space_state.lock().await.graph;
225        let rooms = self.client.joined_space_rooms();
226
227        let mut editable_spaces = Vec::new();
228        for room in &rooms {
229            if let Ok(power_levels) = room.power_levels().await
230                && power_levels.user_can_send_state(user_id, StateEventType::SpaceChild)
231            {
232                let room_id = room.room_id();
233                editable_spaces
234                    .push(SpaceRoom::new_from_known(room, graph.children_of(room_id).len() as u64));
235            }
236        }
237
238        editable_spaces
239    }
240
241    /// Returns a `SpaceRoomList` for the given space ID.
242    pub async fn space_room_list(&self, space_id: OwnedRoomId) -> SpaceRoomList {
243        SpaceRoomList::new(self.client.clone(), space_id).await
244    }
245
246    /// Returns all known direct-parents of a given space room ID.
247    pub async fn joined_parents_of_child(&self, child_id: &RoomId) -> Vec<SpaceRoom> {
248        let graph = &self.space_state.lock().await.graph;
249
250        graph
251            .parents_of(child_id)
252            .into_iter()
253            .filter_map(|parent_id| self.client.get_room(parent_id))
254            .map(|room| {
255                SpaceRoom::new_from_known(&room, graph.children_of(room.room_id()).len() as u64)
256            })
257            .collect()
258    }
259
260    pub async fn add_child_to_space(
261        &self,
262        child_id: OwnedRoomId,
263        space_id: OwnedRoomId,
264    ) -> Result<(), Error> {
265        let user_id = self.client.user_id().ok_or(Error::UserIdNotFound)?;
266        let space_room =
267            self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
268        let child_room =
269            self.client.get_room(&child_id).ok_or(Error::RoomNotFound(child_id.to_owned()))?;
270        let child_power_levels = child_room
271            .power_levels()
272            .await
273            .map_err(|error| Error::UpdateRelationship(matrix_sdk::Error::from(error)))?;
274
275        // Add the child to the space.
276        let child_route = child_room.route().await.map_err(Error::UpdateRelationship)?;
277        space_room
278            .send_state_event_for_key(&child_id, SpaceChildEventContent::new(child_route))
279            .await
280            .map_err(Error::UpdateRelationship)?;
281
282        // Add the space as parent of the child if allowed.
283        if child_power_levels.user_can_send_state(user_id, StateEventType::SpaceParent) {
284            let parent_route = space_room.route().await.map_err(Error::UpdateRelationship)?;
285            child_room
286                .send_state_event_for_key(&space_id, SpaceParentEventContent::new(parent_route))
287                .await
288                .map_err(Error::UpdateRelationship)?;
289        } else {
290            warn!("The current user doesn't have permission to set the child's parent.");
291        }
292
293        Ok(())
294    }
295
296    pub async fn remove_child_from_space(
297        &self,
298        child_id: OwnedRoomId,
299        space_id: OwnedRoomId,
300    ) -> Result<(), Error> {
301        let space_room =
302            self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
303        let child_room =
304            self.client.get_room(&child_id).ok_or(Error::RoomNotFound(child_id.to_owned()))?;
305
306        if let Ok(Some(_)) =
307            space_room.get_state_event_static_for_key::<SpaceChildEventContent, _>(&child_id).await
308        {
309            // Redacting state is a "weird" thing to do, so send {} instead.
310            // https://github.com/matrix-org/matrix-spec/issues/2252
311            //
312            // Specifically, "The redaction of the state doesn't participate in state
313            // resolution so behaves quite differently from e.g. sending an empty form of
314            // that state events".
315            space_room
316                .send_state_event_raw("m.space.child", child_id.as_str(), serde_json::json!({}))
317                .await
318                .map_err(Error::UpdateRelationship)?;
319        } else {
320            warn!("A space child event wasn't found on the parent, ignoring.");
321        }
322
323        if let Ok(Some(_)) =
324            child_room.get_state_event_static_for_key::<SpaceParentEventContent, _>(&space_id).await
325        {
326            // Same as the comment above.
327            child_room
328                .send_state_event_raw("m.space.parent", space_id.as_str(), serde_json::json!({}))
329                .await
330                .map_err(Error::UpdateRelationship)?;
331        } else {
332            warn!("A space parent event wasn't found on the child, ignoring.");
333        }
334
335        Ok(())
336    }
337
338    /// Start a space leave process returning a [`LeaveSpaceHandle`] from which
339    /// rooms can be retrieved in reversed BFS order starting from the requested
340    /// `space_id` graph node. If the room is unknown then an error will be
341    /// returned.
342    ///
343    /// Once the rooms to be left are chosen the handle can be used to leave
344    /// them.
345    pub async fn leave_space(&self, space_id: &RoomId) -> Result<LeaveSpaceHandle, Error> {
346        let space_state = self.space_state.lock().await;
347
348        if !space_state.graph.has_node(space_id) {
349            return Err(Error::RoomNotFound(space_id.to_owned()));
350        }
351
352        let room_ids = space_state.graph.flattened_bottom_up_subtree(space_id);
353
354        let handle = LeaveSpaceHandle::new(self.client.clone(), room_ids).await;
355
356        Ok(handle)
357    }
358
359    async fn update_joined_spaces_if_needed(
360        new_spaces: Vector<SpaceRoom>,
361        new_graph: SpaceGraph,
362        space_state: &Arc<AsyncMutex<SpaceState>>,
363    ) {
364        let mut space_state = space_state.lock().await;
365
366        if new_spaces != space_state.joined_rooms.clone() {
367            space_state.joined_rooms.clear();
368            space_state.joined_rooms.append(new_spaces);
369        }
370
371        space_state.graph = new_graph;
372    }
373
374    async fn joined_spaces_for(client: &Client) -> (Vec<SpaceRoom>, SpaceGraph) {
375        let joined_spaces = client.joined_space_rooms();
376
377        // Build a graph to hold the parent-child relations
378        let mut graph = SpaceGraph::new();
379
380        // Iterate over all joined spaces and populate the graph with edges based
381        // on `m.space.parent` and `m.space.child` state events.
382        for space in joined_spaces.iter() {
383            graph.add_node(space.room_id().to_owned());
384
385            if let Ok(parents) = space.get_state_events_static::<SpaceParentEventContent>().await {
386                parents.into_iter()
387                .flat_map(|parent_event| match parent_event.deserialize() {
388                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(e))) => {
389                        Some(e.state_key)
390                    }
391                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => None,
392                    Ok(SyncOrStrippedState::Stripped(e)) => Some(e.state_key),
393                    Err(e) => {
394                        error!(room_id = ?space.room_id(), "Could not deserialize m.space.parent: {e}");
395                        None
396                    }
397                }).for_each(|parent| graph.add_edge(parent, space.room_id().to_owned()));
398            } else {
399                error!(room_id = ?space.room_id(), "Could not get m.space.parent events");
400            }
401
402            if let Ok(children) = space.get_state_events_static::<SpaceChildEventContent>().await {
403                children.into_iter()
404                .filter_map(|child_event| match child_event.deserialize() {
405                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(e))) => {
406                        Some(e.state_key)
407                    }
408                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => None,
409                    Ok(SyncOrStrippedState::Stripped(e)) => Some(e.state_key),
410                    Err(e) => {
411                        error!(room_id = ?space.room_id(), "Could not deserialize m.space.child: {e}");
412                        None
413                    }
414                }).for_each(|child| graph.add_edge(space.room_id().to_owned(), child));
415            } else {
416                error!(room_id = ?space.room_id(), "Could not get m.space.child events");
417            }
418        }
419
420        // Remove cycles from the graph. This is important because they are not
421        // enforced backend side.
422        graph.remove_cycles();
423
424        let root_nodes = graph.root_nodes();
425
426        // Proceed with filtering to the top level spaces, sorting them by their
427        // (optional) order field (as defined in MSC3230) and then mapping them
428        // to `SpaceRoom`s.
429        let top_level_spaces = joined_spaces
430            .iter()
431            .filter(|room| root_nodes.contains(&room.room_id()))
432            .collect::<Vec<_>>();
433
434        let mut top_level_space_order = HashMap::new();
435        for space in &top_level_spaces {
436            if let Ok(Some(raw_event)) =
437                space.account_data_static::<events::space_order::SpaceOrderEventContent>().await
438                && let Ok(event) = raw_event.deserialize()
439            {
440                top_level_space_order.insert(space.room_id().to_owned(), event.content.order);
441            }
442        }
443
444        let top_level_spaces = top_level_spaces
445            .iter()
446            .sorted_by(|a, b| {
447                // MSC3230: lexicographically by `order` and then by room ID
448                match (
449                    top_level_space_order.get(a.room_id()),
450                    top_level_space_order.get(b.room_id()),
451                ) {
452                    (Some(a_order), Some(b_order)) => {
453                        a_order.cmp(b_order).then(a.room_id().cmp(b.room_id()))
454                    }
455                    (Some(_), None) => Ordering::Less,
456                    (None, Some(_)) => Ordering::Greater,
457                    (None, None) => a.room_id().cmp(b.room_id()),
458                }
459            })
460            .map(|room| {
461                SpaceRoom::new_from_known(room, graph.children_of(room.room_id()).len() as u64)
462            })
463            .collect();
464
465        (top_level_spaces, graph)
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use std::collections::BTreeMap;
472
473    use assert_matches2::assert_let;
474    use eyeball_im::VectorDiff;
475    use futures_util::{StreamExt, pin_mut};
476    use matrix_sdk::{room::ParentSpace, test_utils::mocks::MatrixMockServer};
477    use matrix_sdk_test::{
478        JoinedRoomBuilder, LeftRoomBuilder, RoomAccountDataTestEvent, async_test,
479        event_factory::EventFactory,
480    };
481    use ruma::{RoomVersionId, UserId, event_id, owned_room_id, room_id};
482    use serde_json::json;
483    use stream_assert::{assert_next_eq, assert_pending};
484
485    use super::*;
486
487    #[async_test]
488    async fn test_spaces_hierarchy() {
489        let server = MatrixMockServer::new().await;
490        let client = server.client_builder().build().await;
491        let user_id = client.user_id().unwrap();
492        let space_service = SpaceService::new(client.clone());
493        let factory = EventFactory::new();
494
495        server.mock_room_state_encryption().plain().mount().await;
496
497        // Given one parent space with 2 children spaces
498
499        let parent_space_id = room_id!("!parent_space:example.org");
500        let child_space_id_1 = room_id!("!child_space_1:example.org");
501        let child_space_id_2 = room_id!("!child_space_2:example.org");
502
503        add_space_rooms(
504            vec![
505                MockSpaceRoomParameters {
506                    room_id: child_space_id_1,
507                    order: None,
508                    parents: vec![parent_space_id],
509                    children: vec![],
510                    power_level: None,
511                },
512                MockSpaceRoomParameters {
513                    room_id: child_space_id_2,
514                    order: None,
515                    parents: vec![parent_space_id],
516                    children: vec![],
517                    power_level: None,
518                },
519                MockSpaceRoomParameters {
520                    room_id: parent_space_id,
521                    order: None,
522                    parents: vec![],
523                    children: vec![child_space_id_1, child_space_id_2],
524                    power_level: None,
525                },
526            ],
527            &client,
528            &server,
529            &factory,
530            user_id,
531        )
532        .await;
533
534        // Only the parent space is returned
535        assert_eq!(
536            space_service
537                .joined_spaces()
538                .await
539                .iter()
540                .map(|s| s.room_id.to_owned())
541                .collect::<Vec<_>>(),
542            vec![parent_space_id]
543        );
544
545        // and it has 2 children
546        assert_eq!(
547            space_service
548                .joined_spaces()
549                .await
550                .iter()
551                .map(|s| s.children_count)
552                .collect::<Vec<_>>(),
553            vec![2]
554        );
555
556        let parent_space = client.get_room(parent_space_id).unwrap();
557        assert!(parent_space.is_space());
558
559        // And the parent space and the two child spaces are linked
560
561        let spaces: Vec<ParentSpace> = client
562            .get_room(child_space_id_1)
563            .unwrap()
564            .parent_spaces()
565            .await
566            .unwrap()
567            .map(Result::unwrap)
568            .collect()
569            .await;
570
571        assert_let!(ParentSpace::Reciprocal(parent) = spaces.first().unwrap());
572        assert_eq!(parent.room_id(), parent_space.room_id());
573
574        let spaces: Vec<ParentSpace> = client
575            .get_room(child_space_id_2)
576            .unwrap()
577            .parent_spaces()
578            .await
579            .unwrap()
580            .map(Result::unwrap)
581            .collect()
582            .await;
583
584        assert_let!(ParentSpace::Reciprocal(parent) = spaces.last().unwrap());
585        assert_eq!(parent.room_id(), parent_space.room_id());
586    }
587
588    #[async_test]
589    async fn test_joined_spaces_updates() {
590        let server = MatrixMockServer::new().await;
591        let client = server.client_builder().build().await;
592        let user_id = client.user_id().unwrap();
593        let factory = EventFactory::new();
594
595        server.mock_room_state_encryption().plain().mount().await;
596
597        let first_space_id = room_id!("!first_space:example.org");
598        let second_space_id = room_id!("!second_space:example.org");
599
600        // Join the first space
601        server
602            .sync_room(
603                &client,
604                JoinedRoomBuilder::new(first_space_id)
605                    .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type()),
606            )
607            .await;
608
609        // Build the `SpaceService` and expect the room to show up with no updates
610        // pending
611
612        let space_service = SpaceService::new(client.clone());
613
614        let (initial_values, joined_spaces_subscriber) =
615            space_service.subscribe_to_joined_spaces().await;
616        pin_mut!(joined_spaces_subscriber);
617        assert_pending!(joined_spaces_subscriber);
618
619        assert_eq!(
620            initial_values,
621            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)].into()
622        );
623
624        assert_eq!(
625            space_service.joined_spaces().await,
626            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)]
627        );
628
629        // And the stream is still pending as the initial values were
630        // already set.
631        assert_pending!(joined_spaces_subscriber);
632
633        // Join the second space
634
635        server
636            .sync_room(
637                &client,
638                JoinedRoomBuilder::new(second_space_id)
639                    .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type())
640                    .add_state_event(
641                        factory
642                            .space_child(
643                                second_space_id.to_owned(),
644                                owned_room_id!("!child:example.org"),
645                            )
646                            .sender(user_id),
647                    ),
648            )
649            .await;
650
651        // And expect the list to update
652        assert_eq!(
653            space_service.joined_spaces().await,
654            vec![
655                SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0),
656                SpaceRoom::new_from_known(&client.get_room(second_space_id).unwrap(), 1)
657            ]
658        );
659
660        assert_next_eq!(
661            joined_spaces_subscriber,
662            vec![
663                VectorDiff::Clear,
664                VectorDiff::Append {
665                    values: vec![
666                        SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0),
667                        SpaceRoom::new_from_known(&client.get_room(second_space_id).unwrap(), 1)
668                    ]
669                    .into()
670                },
671            ]
672        );
673
674        server.sync_room(&client, LeftRoomBuilder::new(second_space_id)).await;
675
676        // and when one is left
677        assert_next_eq!(
678            joined_spaces_subscriber,
679            vec![
680                VectorDiff::Clear,
681                VectorDiff::Append {
682                    values: vec![SpaceRoom::new_from_known(
683                        &client.get_room(first_space_id).unwrap(),
684                        0
685                    )]
686                    .into()
687                },
688            ]
689        );
690
691        // but it doesn't when a non-space room gets joined
692        server
693            .sync_room(
694                &client,
695                JoinedRoomBuilder::new(room_id!("!room:example.org"))
696                    .add_state_event(factory.create(user_id, RoomVersionId::V1)),
697            )
698            .await;
699
700        // and the subscriber doesn't yield any updates
701        assert_pending!(joined_spaces_subscriber);
702        assert_eq!(
703            space_service.joined_spaces().await,
704            vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)]
705        );
706    }
707
708    #[async_test]
709    async fn test_top_level_space_order() {
710        let server = MatrixMockServer::new().await;
711        let client = server.client_builder().build().await;
712
713        server.mock_room_state_encryption().plain().mount().await;
714
715        add_space_rooms(
716            vec![
717                MockSpaceRoomParameters {
718                    room_id: room_id!("!2:a.b"),
719                    order: Some("2"),
720                    parents: vec![],
721                    children: vec![],
722                    power_level: None,
723                },
724                MockSpaceRoomParameters {
725                    room_id: room_id!("!4:a.b"),
726                    order: None,
727                    parents: vec![],
728                    children: vec![],
729                    power_level: None,
730                },
731                MockSpaceRoomParameters {
732                    room_id: room_id!("!3:a.b"),
733                    order: None,
734                    parents: vec![],
735                    children: vec![],
736                    power_level: None,
737                },
738                MockSpaceRoomParameters {
739                    room_id: room_id!("!1:a.b"),
740                    order: Some("1"),
741                    parents: vec![],
742                    children: vec![],
743                    power_level: None,
744                },
745            ],
746            &client,
747            &server,
748            &EventFactory::new(),
749            client.user_id().unwrap(),
750        )
751        .await;
752
753        let space_service = SpaceService::new(client.clone());
754
755        // Space with an `order` field set should come first in lexicographic
756        // order and rest sorted by room ID.
757        assert_eq!(
758            space_service.joined_spaces().await,
759            vec![
760                SpaceRoom::new_from_known(&client.get_room(room_id!("!1:a.b")).unwrap(), 0),
761                SpaceRoom::new_from_known(&client.get_room(room_id!("!2:a.b")).unwrap(), 0),
762                SpaceRoom::new_from_known(&client.get_room(room_id!("!3:a.b")).unwrap(), 0),
763                SpaceRoom::new_from_known(&client.get_room(room_id!("!4:a.b")).unwrap(), 0),
764            ]
765        );
766    }
767
768    #[async_test]
769    async fn test_editable_spaces() {
770        // Given a space hierarchy where the user is admin of some spaces and subspaces.
771        let server = MatrixMockServer::new().await;
772        let client = server.client_builder().build().await;
773        let user_id = client.user_id().unwrap();
774        let factory = EventFactory::new();
775
776        server.mock_room_state_encryption().plain().mount().await;
777
778        let admin_space_id = room_id!("!admin_space:example.org");
779        let admin_subspace_id = room_id!("!admin_subspace:example.org");
780        let regular_space_id = room_id!("!regular_space:example.org");
781        let regular_subspace_id = room_id!("!regular_subspace:example.org");
782
783        add_space_rooms(
784            vec![
785                MockSpaceRoomParameters {
786                    room_id: admin_space_id,
787                    order: None,
788                    parents: vec![],
789                    children: vec![regular_subspace_id],
790                    power_level: Some(100),
791                },
792                MockSpaceRoomParameters {
793                    room_id: admin_subspace_id,
794                    order: None,
795                    parents: vec![regular_space_id],
796                    children: vec![],
797                    power_level: Some(100),
798                },
799                MockSpaceRoomParameters {
800                    room_id: regular_space_id,
801                    order: None,
802                    parents: vec![],
803                    children: vec![admin_subspace_id],
804                    power_level: Some(0),
805                },
806                MockSpaceRoomParameters {
807                    room_id: regular_subspace_id,
808                    order: None,
809                    parents: vec![admin_space_id],
810                    children: vec![],
811                    power_level: Some(0),
812                },
813            ],
814            &client,
815            &server,
816            &factory,
817            user_id,
818        )
819        .await;
820
821        let space_service = SpaceService::new(client.clone());
822
823        // Wait for the space hierarchy to register.
824        _ = space_service.joined_spaces().await;
825
826        // When retrieving all editable joined spaces.
827        let editable_spaces = space_service.editable_spaces().await;
828
829        // Then only the spaces where the user is admin are returned.
830        assert_eq!(
831            editable_spaces.iter().map(|room| room.room_id.to_owned()).collect::<Vec<_>>(),
832            vec![admin_space_id.to_owned(), admin_subspace_id.to_owned()]
833        );
834    }
835
836    #[async_test]
837    async fn test_joined_parents_of_child() {
838        // Given a space with three parent spaces, two of which are joined.
839        let server = MatrixMockServer::new().await;
840        let client = server.client_builder().build().await;
841        let user_id = client.user_id().unwrap();
842        let factory = EventFactory::new();
843
844        server.mock_room_state_encryption().plain().mount().await;
845
846        let parent_space_id_1 = room_id!("!parent_space_1:example.org");
847        let parent_space_id_2 = room_id!("!parent_space_2:example.org");
848        let unknown_parent_space_id = room_id!("!unknown_parent_space:example.org");
849        let child_space_id = room_id!("!child_space:example.org");
850
851        add_space_rooms(
852            vec![
853                MockSpaceRoomParameters {
854                    room_id: child_space_id,
855                    order: None,
856                    parents: vec![parent_space_id_1, parent_space_id_2, unknown_parent_space_id],
857                    children: vec![],
858                    power_level: None,
859                },
860                MockSpaceRoomParameters {
861                    room_id: parent_space_id_1,
862                    order: None,
863                    parents: vec![],
864                    children: vec![child_space_id],
865                    power_level: None,
866                },
867                MockSpaceRoomParameters {
868                    room_id: parent_space_id_2,
869                    order: None,
870                    parents: vec![],
871                    children: vec![child_space_id],
872                    power_level: None,
873                },
874            ],
875            &client,
876            &server,
877            &factory,
878            user_id,
879        )
880        .await;
881
882        let space_service = SpaceService::new(client.clone());
883
884        // Wait for the space hierarchy to register.
885        _ = space_service.joined_spaces().await;
886
887        // When retrieving the joined parents of the child space
888        let parents = space_service.joined_parents_of_child(child_space_id).await;
889
890        // Then both parent spaces are returned
891        assert_eq!(
892            parents.iter().map(|space| space.room_id.to_owned()).collect::<Vec<_>>(),
893            vec![parent_space_id_1, parent_space_id_2]
894        );
895    }
896
897    #[async_test]
898    async fn test_add_child_to_space() {
899        // Given a space and child room where the user is admin of both.
900        let server = MatrixMockServer::new().await;
901        let client = server.client_builder().build().await;
902        let user_id = client.user_id().unwrap();
903        let factory = EventFactory::new();
904
905        server.mock_room_state_encryption().plain().mount().await;
906
907        let space_child_event_id = event_id!("$1");
908        let space_parent_event_id = event_id!("$2");
909        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
910        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
911
912        let space_id = room_id!("!my_space:example.org");
913        let child_id = room_id!("!my_child:example.org");
914
915        add_space_rooms(
916            vec![
917                MockSpaceRoomParameters {
918                    room_id: space_id,
919                    order: None,
920                    parents: vec![],
921                    children: vec![],
922                    power_level: Some(100),
923                },
924                MockSpaceRoomParameters {
925                    room_id: child_id,
926                    order: None,
927                    parents: vec![],
928                    children: vec![],
929                    power_level: Some(100),
930                },
931            ],
932            &client,
933            &server,
934            &factory,
935            user_id,
936        )
937        .await;
938
939        let space_service = SpaceService::new(client.clone());
940
941        // When adding the child to the space.
942        let result =
943            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
944
945        // Then both space child and parent events are set successfully.
946        assert!(result.is_ok());
947    }
948
949    #[async_test]
950    async fn test_add_child_to_space_without_space_admin() {
951        // Given a space and child room where the user is a regular member of both.
952        let server = MatrixMockServer::new().await;
953        let client = server.client_builder().build().await;
954        let user_id = client.user_id().unwrap();
955        let factory = EventFactory::new();
956
957        server.mock_room_state_encryption().plain().mount().await;
958
959        server.mock_set_space_child().unauthorized().expect(1).mount().await;
960        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
961
962        let space_id = room_id!("!my_space:example.org");
963        let child_id = room_id!("!my_child:example.org");
964
965        add_space_rooms(
966            vec![
967                MockSpaceRoomParameters {
968                    room_id: space_id,
969                    order: None,
970                    parents: vec![],
971                    children: vec![],
972                    power_level: Some(0),
973                },
974                MockSpaceRoomParameters {
975                    room_id: child_id,
976                    order: None,
977                    parents: vec![],
978                    children: vec![],
979                    power_level: Some(0),
980                },
981            ],
982            &client,
983            &server,
984            &factory,
985            user_id,
986        )
987        .await;
988
989        let space_service = SpaceService::new(client.clone());
990
991        // When adding the child to the space.
992        let result =
993            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
994
995        // Then the operation fails when trying to set the space child event and the
996        // parent event is not attempted.
997        assert!(result.is_err());
998    }
999
1000    #[async_test]
1001    async fn test_add_child_to_space_without_child_admin() {
1002        // Given a space and child room where the user is admin of the space but not of
1003        // the child.
1004        let server = MatrixMockServer::new().await;
1005        let client = server.client_builder().build().await;
1006        let user_id = client.user_id().unwrap();
1007        let factory = EventFactory::new();
1008
1009        server.mock_room_state_encryption().plain().mount().await;
1010
1011        let space_child_event_id = event_id!("$1");
1012        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1013        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1014
1015        let space_id = room_id!("!my_space:example.org");
1016        let child_id = room_id!("!my_child:example.org");
1017
1018        add_space_rooms(
1019            vec![
1020                MockSpaceRoomParameters {
1021                    room_id: space_id,
1022                    order: None,
1023                    parents: vec![],
1024                    children: vec![],
1025                    power_level: Some(100),
1026                },
1027                MockSpaceRoomParameters {
1028                    room_id: child_id,
1029                    order: None,
1030                    parents: vec![],
1031                    children: vec![],
1032                    power_level: Some(0),
1033                },
1034            ],
1035            &client,
1036            &server,
1037            &factory,
1038            user_id,
1039        )
1040        .await;
1041
1042        let space_service = SpaceService::new(client.clone());
1043
1044        // When adding the child to the space.
1045        let result =
1046            space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
1047
1048        error!("result: {:?}", result);
1049        // Then the operation succeeds in setting the space child event and the parent
1050        // event is not attempted.
1051        assert!(result.is_ok());
1052    }
1053
1054    #[async_test]
1055    async fn test_remove_child_from_space() {
1056        // Given a space and child room where the user is admin of both.
1057        let server = MatrixMockServer::new().await;
1058        let client = server.client_builder().build().await;
1059        let user_id = client.user_id().unwrap();
1060        let factory = EventFactory::new();
1061
1062        server.mock_room_state_encryption().plain().mount().await;
1063
1064        let space_child_event_id = event_id!("$1");
1065        let space_parent_event_id = event_id!("$2");
1066        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1067        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1068
1069        let parent_id = room_id!("!parent_space:example.org");
1070        let child_id = room_id!("!child_space:example.org");
1071
1072        add_space_rooms(
1073            vec![
1074                MockSpaceRoomParameters {
1075                    room_id: parent_id,
1076                    order: None,
1077                    parents: vec![],
1078                    children: vec![child_id],
1079                    power_level: None,
1080                },
1081                MockSpaceRoomParameters {
1082                    room_id: child_id,
1083                    order: None,
1084                    parents: vec![parent_id],
1085                    children: vec![],
1086                    power_level: None,
1087                },
1088            ],
1089            &client,
1090            &server,
1091            &factory,
1092            user_id,
1093        )
1094        .await;
1095
1096        let space_service = SpaceService::new(client.clone());
1097
1098        // When removing the child from the space.
1099        let result =
1100            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1101
1102        // Then both space child and parent events are removed successfully.
1103        assert!(result.is_ok());
1104    }
1105
1106    #[async_test]
1107    async fn test_remove_child_from_space_without_parent_event() {
1108        // Given a space with a child where the m.space.parent event wasn't set.
1109        let server = MatrixMockServer::new().await;
1110        let client = server.client_builder().build().await;
1111        let user_id = client.user_id().unwrap();
1112        let factory = EventFactory::new();
1113
1114        server.mock_room_state_encryption().plain().mount().await;
1115
1116        let space_child_event_id = event_id!("$1");
1117        server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
1118        server.mock_set_space_parent().unauthorized().expect(0).mount().await;
1119
1120        let parent_id = room_id!("!parent_space:example.org");
1121        let child_id = room_id!("!child_space:example.org");
1122
1123        add_space_rooms(
1124            vec![
1125                MockSpaceRoomParameters {
1126                    room_id: parent_id,
1127                    order: None,
1128                    parents: vec![],
1129                    children: vec![child_id],
1130                    power_level: None,
1131                },
1132                MockSpaceRoomParameters {
1133                    room_id: child_id,
1134                    order: None,
1135                    parents: vec![],
1136                    children: vec![],
1137                    power_level: None,
1138                },
1139            ],
1140            &client,
1141            &server,
1142            &factory,
1143            user_id,
1144        )
1145        .await;
1146
1147        let space_service = SpaceService::new(client.clone());
1148
1149        // When removing the child from the space.
1150        let result =
1151            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1152
1153        // Then the child event is removed successfully and the parent event removal is
1154        // not attempted.
1155        assert!(result.is_ok());
1156    }
1157
1158    #[async_test]
1159    async fn test_remove_child_from_space_without_child_event() {
1160        // Given a space with a child where the space's m.space.child event wasn't set.
1161        let server = MatrixMockServer::new().await;
1162        let client = server.client_builder().build().await;
1163        let user_id = client.user_id().unwrap();
1164        let factory = EventFactory::new();
1165
1166        server.mock_room_state_encryption().plain().mount().await;
1167
1168        let space_parent_event_id = event_id!("$2");
1169        server.mock_set_space_child().unauthorized().expect(0).mount().await;
1170        server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
1171
1172        let parent_id = room_id!("!parent_space:example.org");
1173        let child_id = room_id!("!child_space:example.org");
1174
1175        add_space_rooms(
1176            vec![
1177                MockSpaceRoomParameters {
1178                    room_id: parent_id,
1179                    order: None,
1180                    parents: vec![],
1181                    children: vec![],
1182                    power_level: None,
1183                },
1184                MockSpaceRoomParameters {
1185                    room_id: child_id,
1186                    order: None,
1187                    parents: vec![parent_id],
1188                    children: vec![],
1189                    power_level: None,
1190                },
1191            ],
1192            &client,
1193            &server,
1194            &factory,
1195            user_id,
1196        )
1197        .await;
1198
1199        let space_service = SpaceService::new(client.clone());
1200
1201        // When removing the child from the space.
1202        let result =
1203            space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
1204
1205        // Then the parent event is removed successfully and the child event removal is
1206        // not attempted.
1207        assert!(result.is_ok());
1208    }
1209
1210    async fn add_space_rooms(
1211        rooms: Vec<MockSpaceRoomParameters>,
1212        client: &Client,
1213        server: &MatrixMockServer,
1214        factory: &EventFactory,
1215        user_id: &UserId,
1216    ) {
1217        for parameters in rooms {
1218            let mut builder = JoinedRoomBuilder::new(parameters.room_id)
1219                .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type());
1220
1221            if let Some(order) = parameters.order {
1222                builder = builder.add_account_data(RoomAccountDataTestEvent::Custom(json!({
1223                    "type": "m.space_order",
1224                      "content": {
1225                        "order": order
1226                      }
1227                })));
1228            }
1229
1230            for parent_id in parameters.parents {
1231                builder = builder.add_state_event(
1232                    factory
1233                        .space_parent(parent_id.to_owned(), parameters.room_id.to_owned())
1234                        .sender(user_id),
1235                );
1236            }
1237
1238            for child_id in parameters.children {
1239                builder = builder.add_state_event(
1240                    factory
1241                        .space_child(parameters.room_id.to_owned(), child_id.to_owned())
1242                        .sender(user_id),
1243                );
1244            }
1245
1246            if let Some(power_level) = parameters.power_level {
1247                let mut power_levels = BTreeMap::from([(user_id.to_owned(), power_level.into())]);
1248
1249                builder = builder.add_state_event(
1250                    factory.power_levels(&mut power_levels).state_key("").sender(user_id),
1251                );
1252            }
1253
1254            server.sync_room(client, builder).await;
1255        }
1256    }
1257
1258    struct MockSpaceRoomParameters {
1259        room_id: &'static RoomId,
1260        order: Option<&'static str>,
1261        parents: Vec<&'static RoomId>,
1262        children: Vec<&'static RoomId>,
1263        power_level: Option<i32>,
1264    }
1265}