matrix_sdk_ui/spaces/
leave.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
15use matrix_sdk::{Client, ROOM_VERSION_RULES_FALLBACK, RoomState, room::RoomMemberRole};
16use ruma::{Int, OwnedRoomId, events::room::member::MembershipState};
17use tracing::info;
18
19use crate::spaces::{Error, SpaceRoom};
20
21/// Space leaving specific room that groups normal [`SpaceRoom`] details with
22/// information about the leaving user's role.
23#[derive(Debug, Clone)]
24pub struct LeaveSpaceRoom {
25    /// The underlying [`SpaceRoom`]
26    pub space_room: SpaceRoom,
27    /// Whether the user is the last owner in the room. This helps clients
28    /// better inform the user about the consequences of leaving the room.
29    pub is_last_owner: bool,
30    /// If the room creators have infinite PL.
31    pub are_creators_privileged: bool,
32}
33
34/// The `LeaveSpaceHandle` processes rooms to be left in the order they were
35/// provided by the [`crate::spaces::SpaceService`] and annotates them with
36/// extra data to inform the leave process e.g. if the current user is the last
37/// room owner.
38///
39/// Once the upstream client decides what rooms should actually be left, the
40/// handle provides a method to execute that too.
41pub struct LeaveSpaceHandle {
42    client: Client,
43    rooms: Vec<LeaveSpaceRoom>,
44}
45
46impl LeaveSpaceHandle {
47    pub(crate) async fn new(client: Client, room_ids: Vec<OwnedRoomId>) -> Self {
48        let mut rooms = Vec::new();
49
50        for room_id in &room_ids {
51            let Some(room) = client.get_room(room_id) else {
52                continue;
53            };
54
55            if room.state() != RoomState::Joined {
56                continue;
57            }
58
59            if !room.are_members_synced() {
60                info!("Syncing members for room {} to check if can leave", room.room_id());
61                _ = room.sync_members().await.ok();
62            }
63
64            let mut privileged_creator_ids = Vec::new();
65            let mut are_creators_privileged = false;
66            if let Some(mut create) = room.create_content() {
67                let rules = create.room_version.rules().unwrap_or(ROOM_VERSION_RULES_FALLBACK);
68                if rules.authorization.explicitly_privilege_room_creators {
69                    are_creators_privileged = true;
70                    privileged_creator_ids.push(create.creator);
71                    privileged_creator_ids.append(&mut create.additional_creators);
72                }
73            }
74
75            let owner_ids = room
76                .users_with_power_levels()
77                .await
78                .into_iter()
79                .filter(|(_, power_level)| {
80                    let Some(power_level) = Int::new(*power_level) else {
81                        return false;
82                    };
83
84                    if are_creators_privileged {
85                        power_level >= ruma::int!(150)
86                    } else {
87                        RoomMemberRole::suggested_role_for_power_level(power_level.into())
88                            == RoomMemberRole::Administrator
89                    }
90                })
91                .map(|p: (ruma::OwnedUserId, i64)| p.0)
92                .chain(privileged_creator_ids.into_iter());
93
94            let mut joined_owner_ids = Vec::new();
95            for owner_id in owner_ids {
96                if let Ok(Some(member)) = room.get_member_no_sync(&owner_id).await
97                    && *member.membership() == MembershipState::Join
98                {
99                    joined_owner_ids.push(owner_id);
100                }
101            }
102            let is_last_owner = joined_owner_ids == [room.own_user_id()];
103
104            rooms.push(LeaveSpaceRoom {
105                space_room: SpaceRoom::new_from_known(&room, 0),
106                is_last_owner,
107                are_creators_privileged,
108            });
109        }
110
111        Self { client, rooms }
112    }
113
114    /// A list of rooms to be left which next to normal [`SpaceRoom`] data also
115    /// include leave specific information.
116    pub fn rooms(&self) -> &Vec<LeaveSpaceRoom> {
117        &self.rooms
118    }
119
120    /// Bulk leave the given rooms. Stops when encountering an error.
121    pub async fn leave(&self, filter: impl FnMut(&LeaveSpaceRoom) -> bool) -> Result<(), Error> {
122        for room in self.rooms.clone().into_iter().filter(filter) {
123            if let Some(room) = self.client.get_room(&room.space_room.room_id) {
124                room.leave().await.map_err(Error::LeaveSpace)?;
125            } else {
126                return Err(Error::RoomNotFound(room.space_room.room_id));
127            }
128        }
129
130        Ok(())
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use std::collections::BTreeMap;
137
138    use matrix_sdk::test_utils::mocks::MatrixMockServer;
139    use matrix_sdk_test::{
140        InvitedRoomBuilder, JoinedRoomBuilder, LeftRoomBuilder, async_test,
141        event_factory::EventFactory,
142    };
143    use ruma::{RoomVersionId, owned_user_id, room_id};
144
145    use crate::spaces::SpaceService;
146
147    #[async_test]
148    async fn test_leaving() {
149        let server = MatrixMockServer::new().await;
150        let client = server.client_builder().build().await;
151        let user_id = client.user_id().unwrap();
152        let space_service = SpaceService::new(client.clone()).await;
153        let factory = EventFactory::new().sender(user_id);
154
155        server.mock_room_state_encryption().plain().mount().await;
156
157        // Given one parent space with 2 children spaces
158
159        let parent_space_id = room_id!("!parent_space:example.org");
160        let child_space_id_1 = room_id!("!child_space_1:example.org");
161        let child_space_id_2 = room_id!("!child_space_2:example.org");
162        let child_space_v12_id_1 = room_id!("!child_space_v12_1:example.org");
163        let child_space_v12_id_2 = room_id!("!child_space_v12_2:example.org");
164        let left_room_id = room_id!("!left_room:example.org");
165        let invited_room_id = room_id!("!invited_room:example.org");
166
167        let some_non_admin_id = owned_user_id!("@some_non_admin:a.b");
168        let mut power_levels = BTreeMap::from([
169            (user_id.to_owned(), 100.into()),
170            (some_non_admin_id.clone(), 50.into()),
171        ]);
172
173        server
174            .sync_room(
175                &client,
176                JoinedRoomBuilder::new(child_space_id_1)
177                    .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type())
178                    .add_state_event(
179                        factory
180                            .space_parent(parent_space_id.to_owned(), child_space_id_1.to_owned()),
181                    )
182                    .add_state_event(
183                        factory.power_levels(&mut power_levels).state_key("").sender(user_id),
184                    )
185                    .add_state_event(factory.member(user_id).state_key(user_id.to_string()))
186                    .add_state_event(
187                        factory.member(&some_non_admin_id).state_key(some_non_admin_id.to_string()),
188                    )
189                    .set_joined_members_count(2),
190            )
191            .await;
192
193        let other_admin_user_id = owned_user_id!("@some_other_admin:a.b");
194        power_levels.insert(other_admin_user_id.clone(), 100.into());
195
196        server
197            .sync_room(
198                &client,
199                JoinedRoomBuilder::new(child_space_id_2)
200                    .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type())
201                    .add_state_event(
202                        factory
203                            .space_parent(parent_space_id.to_owned(), child_space_id_2.to_owned()),
204                    )
205                    .add_state_event(
206                        factory.power_levels(&mut power_levels).state_key("").sender(user_id),
207                    )
208                    .add_state_event(factory.member(user_id).state_key(user_id.to_string()))
209                    .add_state_event(
210                        factory.member(&some_non_admin_id).state_key(some_non_admin_id.to_string()),
211                    )
212                    .add_state_event(
213                        factory
214                            .member(&other_admin_user_id)
215                            .state_key(other_admin_user_id.to_string()),
216                    )
217                    .set_joined_members_count(3),
218            )
219            .await;
220
221        // The creator is not supposed to be in the power levels on v12
222        let mut power_levels = BTreeMap::from([
223            (some_non_admin_id.clone(), 50.into()),
224            (other_admin_user_id.clone(), 100.into()),
225        ]);
226        server
227            .sync_room(
228                &client,
229                JoinedRoomBuilder::new(child_space_v12_id_1)
230                    .add_state_event(factory.create(user_id, RoomVersionId::V12).with_space_type())
231                    .add_state_event(
232                        factory.space_parent(
233                            parent_space_id.to_owned(),
234                            child_space_v12_id_1.to_owned(),
235                        ),
236                    )
237                    .add_state_event(factory.member(user_id).state_key(user_id.to_string()))
238                    .add_state_event(
239                        factory.member(&some_non_admin_id).state_key(some_non_admin_id.to_string()),
240                    )
241                    .add_state_event(
242                        factory
243                            .member(&other_admin_user_id)
244                            .state_key(other_admin_user_id.to_string()),
245                    )
246                    .add_state_event(
247                        factory.power_levels(&mut power_levels).state_key("").sender(user_id),
248                    )
249                    .set_joined_members_count(3),
250            )
251            .await;
252
253        let other_owner_user_id = owned_user_id!("@some_other_owner:a.b");
254        power_levels.insert(other_owner_user_id.clone(), 150.into());
255        server
256            .sync_room(
257                &client,
258                JoinedRoomBuilder::new(child_space_v12_id_2)
259                    .add_state_event(factory.create(user_id, RoomVersionId::V12).with_space_type())
260                    .add_state_event(
261                        factory.space_parent(
262                            parent_space_id.to_owned(),
263                            child_space_v12_id_2.to_owned(),
264                        ),
265                    )
266                    .add_state_event(factory.member(user_id).state_key(user_id.to_string()))
267                    .add_state_event(
268                        factory.member(&some_non_admin_id).state_key(some_non_admin_id.to_string()),
269                    )
270                    .add_state_event(
271                        factory
272                            .member(&other_admin_user_id)
273                            .state_key(other_admin_user_id.to_string()),
274                    )
275                    .add_state_event(
276                        factory
277                            .member(&other_owner_user_id)
278                            .state_key(other_owner_user_id.to_string()),
279                    )
280                    .add_state_event(
281                        factory.power_levels(&mut power_levels).state_key("").sender(user_id),
282                    )
283                    .set_joined_members_count(4),
284            )
285            .await;
286
287        server.sync_room(&client, LeftRoomBuilder::new(invited_room_id)).await;
288        server.sync_room(&client, InvitedRoomBuilder::new(invited_room_id)).await;
289
290        server
291            .sync_room(
292                &client,
293                JoinedRoomBuilder::new(parent_space_id)
294                    .add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type())
295                    .add_state_event(
296                        factory
297                            .space_child(parent_space_id.to_owned(), child_space_id_1.to_owned()),
298                    )
299                    .add_state_event(
300                        factory
301                            .space_child(parent_space_id.to_owned(), child_space_id_2.to_owned()),
302                    )
303                    .add_state_event(
304                        factory.space_child(
305                            parent_space_id.to_owned(),
306                            child_space_v12_id_1.to_owned(),
307                        ),
308                    )
309                    .add_state_event(
310                        factory.space_child(
311                            parent_space_id.to_owned(),
312                            child_space_v12_id_2.to_owned(),
313                        ),
314                    )
315                    .add_state_event(
316                        factory.space_child(parent_space_id.to_owned(), left_room_id.to_owned()),
317                    )
318                    .add_state_event(
319                        factory.space_child(parent_space_id.to_owned(), invited_room_id.to_owned()),
320                    ),
321            )
322            .await;
323
324        server.mock_room_leave().ok(room_id!("!does_not_matter:a:b")).mount().await;
325
326        assert!(!space_service.top_level_joined_spaces().await.is_empty());
327
328        let handle = space_service.leave_space(parent_space_id).await.unwrap();
329
330        let rooms = handle.rooms();
331
332        let child_room_1 = &rooms[0];
333        assert!(child_room_1.is_last_owner);
334        assert_eq!(child_room_1.space_room.num_joined_members, 2);
335        assert!(!child_room_1.are_creators_privileged);
336
337        let child_room_2 = &rooms[1];
338        assert!(!child_room_2.is_last_owner);
339        assert_eq!(child_room_2.space_room.num_joined_members, 3);
340        assert!(!child_room_2.are_creators_privileged);
341
342        let child_room_3 = &rooms[2];
343        assert!(child_room_3.is_last_owner);
344        assert_eq!(child_room_3.space_room.num_joined_members, 3);
345        assert!(child_room_3.are_creators_privileged);
346
347        let child_room_4 = &rooms[3];
348        assert!(!child_room_4.is_last_owner);
349        assert_eq!(child_room_4.space_room.num_joined_members, 4);
350        assert!(child_room_4.are_creators_privileged);
351
352        let room_ids = rooms.iter().map(|r| r.space_room.room_id.clone()).collect::<Vec<_>>();
353        assert_eq!(
354            room_ids,
355            vec![
356                child_space_id_1,
357                child_space_id_2,
358                child_space_v12_id_1,
359                child_space_v12_id_2,
360                parent_space_id
361            ]
362        );
363
364        handle.leave(|room| room_ids.contains(&room.space_room.room_id)).await.unwrap();
365
366        assert!(space_service.top_level_joined_spaces().await.is_empty());
367    }
368}