1use 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#[derive(Debug, Clone)]
24pub struct LeaveSpaceRoom {
25 pub space_room: SpaceRoom,
27 pub is_last_owner: bool,
30 pub are_creators_privileged: bool,
32}
33
34pub 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 pub fn rooms(&self) -> &Vec<LeaveSpaceRoom> {
117 &self.rooms
118 }
119
120 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 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 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}