use matrix_sdk::{Client, ROOM_VERSION_RULES_FALLBACK, RoomState, room::RoomMemberRole};
use ruma::{Int, OwnedRoomId, events::room::member::MembershipState};
use tracing::info;
use crate::spaces::{Error, SpaceRoom};
#[derive(Debug, Clone)]
pub struct LeaveSpaceRoom {
pub space_room: SpaceRoom,
pub is_last_owner: bool,
pub are_creators_privileged: bool,
}
pub struct LeaveSpaceHandle {
client: Client,
rooms: Vec<LeaveSpaceRoom>,
}
impl LeaveSpaceHandle {
pub(crate) async fn new(client: Client, room_ids: Vec<OwnedRoomId>) -> Self {
let mut rooms = Vec::new();
for room_id in &room_ids {
let Some(room) = client.get_room(room_id) else {
continue;
};
if room.state() != RoomState::Joined {
continue;
}
if !room.are_members_synced() {
info!("Syncing members for room {} to check if can leave", room.room_id());
_ = room.sync_members().await.ok();
}
let mut privileged_creator_ids = Vec::new();
let mut are_creators_privileged = false;
if let Some(mut create) = room.create_content() {
let rules = create.room_version.rules().unwrap_or(ROOM_VERSION_RULES_FALLBACK);
if rules.authorization.explicitly_privilege_room_creators {
are_creators_privileged = true;
privileged_creator_ids.push(create.creator);
privileged_creator_ids.append(&mut create.additional_creators);
}
}
let owner_ids = room
.users_with_power_levels()
.await
.into_iter()
.filter(|(_, power_level)| {
let Some(power_level) = Int::new(*power_level) else {
return false;
};
if are_creators_privileged {
power_level >= ruma::int!(150)
} else {
RoomMemberRole::suggested_role_for_power_level(power_level.into())
== RoomMemberRole::Administrator
}
})
.map(|p: (ruma::OwnedUserId, i64)| p.0)
.chain(privileged_creator_ids);
let mut joined_owner_ids = Vec::new();
for owner_id in owner_ids {
if let Ok(Some(member)) = room.get_member_no_sync(&owner_id).await
&& *member.membership() == MembershipState::Join
{
joined_owner_ids.push(owner_id);
}
}
let is_last_owner = joined_owner_ids == [room.own_user_id()];
rooms.push(LeaveSpaceRoom {
space_room: SpaceRoom::new_from_known(&room, 0),
is_last_owner,
are_creators_privileged,
});
}
Self { client, rooms }
}
pub fn rooms(&self) -> &Vec<LeaveSpaceRoom> {
&self.rooms
}
pub async fn leave(&self, filter: impl FnMut(&LeaveSpaceRoom) -> bool) -> Result<(), Error> {
for room in self.rooms.clone().into_iter().filter(filter) {
if let Some(room) = self.client.get_room(&room.space_room.room_id) {
room.leave().await.map_err(Error::LeaveSpace)?;
} else {
return Err(Error::RoomNotFound(room.space_room.room_id));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use matrix_sdk::test_utils::mocks::MatrixMockServer;
use matrix_sdk_test::{
InvitedRoomBuilder, JoinedRoomBuilder, LeftRoomBuilder, async_test,
event_factory::EventFactory,
};
use ruma::{RoomVersionId, owned_user_id, room_id};
use crate::spaces::SpaceService;
#[async_test]
async fn test_leaving() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let space_service = SpaceService::new(client.clone()).await;
let factory = EventFactory::new().sender(user_id);
server.mock_room_state_encryption().plain().mount().await;
let parent_space_id = room_id!("!parent_space:example.org");
let child_space_id_1 = room_id!("!child_space_1:example.org");
let child_space_id_2 = room_id!("!child_space_2:example.org");
let child_space_v12_id_1 = room_id!("!child_space_v12_1:example.org");
let child_space_v12_id_2 = room_id!("!child_space_v12_2:example.org");
let left_room_id = room_id!("!left_room:example.org");
let invited_room_id = room_id!("!invited_room:example.org");
let some_non_admin_id = owned_user_id!("@some_non_admin:a.b");
let mut power_levels = BTreeMap::from([
(user_id.to_owned(), 100.into()),
(some_non_admin_id.clone(), 50.into()),
]);
server
.sync_room(
&client,
JoinedRoomBuilder::new(child_space_id_1)
.add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type())
.add_state_event(
factory
.space_parent(parent_space_id.to_owned(), child_space_id_1.to_owned()),
)
.add_state_event(
factory.power_levels(&mut power_levels).state_key("").sender(user_id),
)
.add_state_event(factory.member(user_id).state_key(user_id.to_string()))
.add_state_event(
factory.member(&some_non_admin_id).state_key(some_non_admin_id.to_string()),
)
.set_joined_members_count(2),
)
.await;
let other_admin_user_id = owned_user_id!("@some_other_admin:a.b");
power_levels.insert(other_admin_user_id.clone(), 100.into());
server
.sync_room(
&client,
JoinedRoomBuilder::new(child_space_id_2)
.add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type())
.add_state_event(
factory
.space_parent(parent_space_id.to_owned(), child_space_id_2.to_owned()),
)
.add_state_event(
factory.power_levels(&mut power_levels).state_key("").sender(user_id),
)
.add_state_event(factory.member(user_id).state_key(user_id.to_string()))
.add_state_event(
factory.member(&some_non_admin_id).state_key(some_non_admin_id.to_string()),
)
.add_state_event(
factory
.member(&other_admin_user_id)
.state_key(other_admin_user_id.to_string()),
)
.set_joined_members_count(3),
)
.await;
let mut power_levels = BTreeMap::from([
(some_non_admin_id.clone(), 50.into()),
(other_admin_user_id.clone(), 100.into()),
]);
server
.sync_room(
&client,
JoinedRoomBuilder::new(child_space_v12_id_1)
.add_state_event(factory.create(user_id, RoomVersionId::V12).with_space_type())
.add_state_event(
factory.space_parent(
parent_space_id.to_owned(),
child_space_v12_id_1.to_owned(),
),
)
.add_state_event(factory.member(user_id).state_key(user_id.to_string()))
.add_state_event(
factory.member(&some_non_admin_id).state_key(some_non_admin_id.to_string()),
)
.add_state_event(
factory
.member(&other_admin_user_id)
.state_key(other_admin_user_id.to_string()),
)
.add_state_event(
factory.power_levels(&mut power_levels).state_key("").sender(user_id),
)
.set_joined_members_count(3),
)
.await;
let other_owner_user_id = owned_user_id!("@some_other_owner:a.b");
power_levels.insert(other_owner_user_id.clone(), 150.into());
server
.sync_room(
&client,
JoinedRoomBuilder::new(child_space_v12_id_2)
.add_state_event(factory.create(user_id, RoomVersionId::V12).with_space_type())
.add_state_event(
factory.space_parent(
parent_space_id.to_owned(),
child_space_v12_id_2.to_owned(),
),
)
.add_state_event(factory.member(user_id).state_key(user_id.to_string()))
.add_state_event(
factory.member(&some_non_admin_id).state_key(some_non_admin_id.to_string()),
)
.add_state_event(
factory
.member(&other_admin_user_id)
.state_key(other_admin_user_id.to_string()),
)
.add_state_event(
factory
.member(&other_owner_user_id)
.state_key(other_owner_user_id.to_string()),
)
.add_state_event(
factory.power_levels(&mut power_levels).state_key("").sender(user_id),
)
.set_joined_members_count(4),
)
.await;
server.sync_room(&client, LeftRoomBuilder::new(invited_room_id)).await;
server.sync_room(&client, InvitedRoomBuilder::new(invited_room_id)).await;
server
.sync_room(
&client,
JoinedRoomBuilder::new(parent_space_id)
.add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type())
.add_state_event(
factory
.space_child(parent_space_id.to_owned(), child_space_id_1.to_owned()),
)
.add_state_event(
factory
.space_child(parent_space_id.to_owned(), child_space_id_2.to_owned()),
)
.add_state_event(
factory.space_child(
parent_space_id.to_owned(),
child_space_v12_id_1.to_owned(),
),
)
.add_state_event(
factory.space_child(
parent_space_id.to_owned(),
child_space_v12_id_2.to_owned(),
),
)
.add_state_event(
factory.space_child(parent_space_id.to_owned(), left_room_id.to_owned()),
)
.add_state_event(
factory.space_child(parent_space_id.to_owned(), invited_room_id.to_owned()),
),
)
.await;
server.mock_room_leave().ok(room_id!("!does_not_matter:a:b")).mount().await;
assert!(!space_service.top_level_joined_spaces().await.is_empty());
let handle = space_service.leave_space(parent_space_id).await.unwrap();
let rooms = handle.rooms();
let child_room_1 = &rooms[0];
assert!(child_room_1.is_last_owner);
assert_eq!(child_room_1.space_room.num_joined_members, 2);
assert!(!child_room_1.are_creators_privileged);
let child_room_2 = &rooms[1];
assert!(!child_room_2.is_last_owner);
assert_eq!(child_room_2.space_room.num_joined_members, 3);
assert!(!child_room_2.are_creators_privileged);
let child_room_3 = &rooms[2];
assert!(child_room_3.is_last_owner);
assert_eq!(child_room_3.space_room.num_joined_members, 3);
assert!(child_room_3.are_creators_privileged);
let child_room_4 = &rooms[3];
assert!(!child_room_4.is_last_owner);
assert_eq!(child_room_4.space_room.num_joined_members, 4);
assert!(child_room_4.are_creators_privileged);
let room_ids = rooms.iter().map(|r| r.space_room.room_id.clone()).collect::<Vec<_>>();
assert_eq!(
room_ids,
vec![
child_space_id_1,
child_space_id_2,
child_space_v12_id_1,
child_space_v12_id_2,
parent_space_id
]
);
handle.leave(|room| room_ids.contains(&room.space_room.room_id)).await.unwrap();
assert!(space_service.top_level_joined_spaces().await.is_empty());
}
}