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