use std::{cmp::Ordering, collections::HashMap, sync::Arc};
use eyeball_im::{ObservableVector, VectorSubscriberBatchedStream};
use futures_util::pin_mut;
use imbl::Vector;
use itertools::Itertools;
use matrix_sdk::{
Client, Error as SDKError, Room, deserialized_responses::SyncOrStrippedState,
task_monitor::BackgroundTaskHandle,
};
use ruma::{
OwnedRoomId, RoomId, SpaceChildOrder,
events::{
self, StateEventType, SyncStateEvent,
space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
},
};
use thiserror::Error;
use tokio::sync::Mutex as AsyncMutex;
use tracing::{error, trace, warn};
use crate::spaces::{graph::SpaceGraph, leave::LeaveSpaceHandle, room::SpaceRoomChildState};
pub use crate::spaces::{room::SpaceRoom, room_list::SpaceRoomList};
pub mod graph;
pub mod leave;
pub mod room;
pub mod room_list;
#[derive(Debug, Error)]
pub enum Error {
#[error("User ID not available from client")]
UserIdNotFound,
#[error("Room `{0}` not found")]
RoomNotFound(OwnedRoomId),
#[error("Missing `{0}` for `{1}`")]
MissingState(StateEventType, OwnedRoomId),
#[error("Failed to set either of the m.space.parent or m.space.child state events")]
UpdateRelationship(SDKError),
#[error(
"Failed to set the expected m.space.parent state event (but any m.space.child changes were successful)"
)]
UpdateInverseRelationship(SDKError),
#[error("Failed to leave space")]
LeaveSpace(SDKError),
#[error("Failed to load members")]
LoadRoomMembers(SDKError),
}
struct SpaceState {
graph: SpaceGraph,
top_level_joined_spaces: ObservableVector<SpaceRoom>,
space_filters: ObservableVector<SpaceFilter>,
}
pub struct SpaceService {
client: Client,
space_state: Arc<AsyncMutex<SpaceState>>,
_room_update_handle: AsyncMutex<BackgroundTaskHandle>,
}
impl SpaceService {
pub async fn new(client: Client) -> Self {
let space_state = Arc::new(AsyncMutex::new(SpaceState {
graph: SpaceGraph::new(),
top_level_joined_spaces: ObservableVector::new(),
space_filters: ObservableVector::new(),
}));
let room_update_handle = client
.task_monitor()
.spawn_infinite_task("space_service", {
let client = client.clone();
let space_state = Arc::clone(&space_state);
let all_room_updates_receiver = client.subscribe_to_all_room_updates();
async move {
pin_mut!(all_room_updates_receiver);
loop {
match all_room_updates_receiver.recv().await {
Ok(updates) => {
if updates.is_empty() {
continue;
}
let (spaces, filters, graph) =
Self::build_space_state(&client).await;
Self::update_space_state_if_needed(
Vector::from(spaces),
Vector::from(filters),
graph,
&space_state,
)
.await;
}
Err(err) => {
error!("error when listening to room updates: {err}");
}
}
}
}
})
.abort_on_drop();
let (spaces, filters, graph) = Self::build_space_state(&client).await;
Self::update_space_state_if_needed(
Vector::from(spaces),
Vector::from(filters),
graph,
&space_state,
)
.await;
Self { client, space_state, _room_update_handle: AsyncMutex::new(room_update_handle) }
}
pub async fn subscribe_to_top_level_joined_spaces(
&self,
) -> (Vector<SpaceRoom>, VectorSubscriberBatchedStream<SpaceRoom>) {
self.space_state
.lock()
.await
.top_level_joined_spaces
.subscribe()
.into_values_and_batched_stream()
}
pub async fn top_level_joined_spaces(&self) -> Vec<SpaceRoom> {
let (top_level_joined_spaces, filters, graph) = Self::build_space_state(&self.client).await;
Self::update_space_state_if_needed(
Vector::from(top_level_joined_spaces.clone()),
Vector::from(filters),
graph,
&self.space_state,
)
.await;
top_level_joined_spaces
}
pub async fn space_filters(&self) -> Vec<SpaceFilter> {
let (top_level_joined_spaces, filters, graph) = Self::build_space_state(&self.client).await;
Self::update_space_state_if_needed(
Vector::from(top_level_joined_spaces),
Vector::from(filters.clone()),
graph,
&self.space_state,
)
.await;
filters
}
pub async fn subscribe_to_space_filters(
&self,
) -> (Vector<SpaceFilter>, VectorSubscriberBatchedStream<SpaceFilter>) {
self.space_state.lock().await.space_filters.subscribe().into_values_and_batched_stream()
}
pub async fn editable_spaces(&self) -> Vec<SpaceRoom> {
let Some(user_id) = self.client.user_id() else {
return vec![];
};
let graph = &self.space_state.lock().await.graph;
let rooms = self.client.joined_space_rooms();
let mut editable_spaces = Vec::new();
for room in &rooms {
if let Ok(power_levels) = room.power_levels().await
&& power_levels.user_can_send_state(user_id, StateEventType::SpaceChild)
{
let room_id = room.room_id();
editable_spaces
.push(SpaceRoom::new_from_known(room, graph.children_of(room_id).len() as u64));
}
}
editable_spaces
}
pub async fn space_room_list(&self, space_id: OwnedRoomId) -> SpaceRoomList {
SpaceRoomList::new(self.client.clone(), space_id).await
}
pub async fn joined_parents_of_child(&self, child_id: &RoomId) -> Vec<SpaceRoom> {
let graph = &self.space_state.lock().await.graph;
graph
.parents_of(child_id)
.into_iter()
.filter_map(|parent_id| self.client.get_room(parent_id))
.map(|room| {
SpaceRoom::new_from_known(&room, graph.children_of(room.room_id()).len() as u64)
})
.collect()
}
pub async fn get_space_room(&self, room_id: &RoomId) -> Option<SpaceRoom> {
let graph = &self.space_state.lock().await.graph;
if graph.has_node(room_id)
&& let Some(room) = self.client.get_room(room_id)
{
Some(SpaceRoom::new_from_known(&room, graph.children_of(room.room_id()).len() as u64))
} else {
None
}
}
pub async fn add_child_to_space(
&self,
child_id: OwnedRoomId,
space_id: OwnedRoomId,
) -> Result<(), Error> {
let user_id = self.client.user_id().ok_or(Error::UserIdNotFound)?;
let space_room =
self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
let child_room =
self.client.get_room(&child_id).ok_or(Error::RoomNotFound(child_id.to_owned()))?;
let child_power_levels = child_room
.power_levels()
.await
.map_err(|error| Error::UpdateRelationship(matrix_sdk::Error::from(error)))?;
let child_route = child_room.route().await.map_err(Error::UpdateRelationship)?;
space_room
.send_state_event_for_key(&child_id, SpaceChildEventContent::new(child_route))
.await
.map_err(Error::UpdateRelationship)?;
if child_power_levels.user_can_send_state(user_id, StateEventType::SpaceParent) {
let parent_route =
space_room.route().await.map_err(Error::UpdateInverseRelationship)?;
child_room
.send_state_event_for_key(&space_id, SpaceParentEventContent::new(parent_route))
.await
.map_err(Error::UpdateInverseRelationship)?;
} else {
warn!("The current user doesn't have permission to set the child's parent.");
}
Ok(())
}
pub async fn remove_child_from_space(
&self,
child_id: OwnedRoomId,
space_id: OwnedRoomId,
) -> Result<(), Error> {
let user_id = self.client.user_id().ok_or(Error::UserIdNotFound)?;
let space_room =
self.client.get_room(&space_id).ok_or(Error::RoomNotFound(space_id.to_owned()))?;
if let Ok(Some(_)) =
space_room.get_state_event_static_for_key::<SpaceChildEventContent, _>(&child_id).await
{
space_room
.send_state_event_raw("m.space.child", child_id.as_str(), serde_json::json!({}))
.await
.map_err(Error::UpdateRelationship)?;
} else {
warn!("A space child event wasn't found on the parent, ignoring.");
}
if let Some(child_room) = self.client.get_room(&child_id) {
let power_levels = child_room.power_levels().await.map_err(|error| {
Error::UpdateInverseRelationship(matrix_sdk::Error::from(error))
})?;
if power_levels.user_can_send_state(user_id, StateEventType::SpaceParent)
&& let Ok(Some(_)) = child_room
.get_state_event_static_for_key::<SpaceParentEventContent, _>(&space_id)
.await
{
child_room
.send_state_event_raw(
"m.space.parent",
space_id.as_str(),
serde_json::json!({}),
)
.await
.map_err(Error::UpdateInverseRelationship)?;
} else {
warn!("A space parent event wasn't found on the child, ignoring.");
}
} else {
warn!("The child room is unknown, skipping m.space.parent removal.");
}
Ok(())
}
pub async fn leave_space(&self, space_id: &RoomId) -> Result<LeaveSpaceHandle, Error> {
let space_state = self.space_state.lock().await;
if !space_state.graph.has_node(space_id) {
return Err(Error::RoomNotFound(space_id.to_owned()));
}
let room_ids = space_state.graph.flattened_bottom_up_subtree(space_id);
let handle = LeaveSpaceHandle::new(self.client.clone(), room_ids).await;
Ok(handle)
}
async fn update_space_state_if_needed(
new_spaces: Vector<SpaceRoom>,
new_filters: Vector<SpaceFilter>,
new_graph: SpaceGraph,
space_state: &Arc<AsyncMutex<SpaceState>>,
) {
let mut space_state = space_state.lock().await;
if new_spaces != space_state.top_level_joined_spaces.clone() {
space_state.top_level_joined_spaces.clear();
space_state.top_level_joined_spaces.append(new_spaces);
}
if new_filters != space_state.space_filters.clone() {
space_state.space_filters.clear();
space_state.space_filters.append(new_filters);
}
space_state.graph = new_graph;
}
async fn build_space_state(client: &Client) -> (Vec<SpaceRoom>, Vec<SpaceFilter>, SpaceGraph) {
let joined_spaces = client.joined_space_rooms();
let mut graph = SpaceGraph::new();
let mut space_child_states = HashMap::<OwnedRoomId, SpaceRoomChildState>::new();
for space in joined_spaces.iter() {
graph.add_node(space.room_id().to_owned());
if let Ok(parents) = space.get_state_events_static::<SpaceParentEventContent>().await {
parents.into_iter()
.flat_map(|parent_event| match parent_event.deserialize() {
Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(e))) => {
Some(e.state_key)
}
Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => None,
Ok(SyncOrStrippedState::Stripped(e)) => Some(e.state_key),
Err(e) => {
trace!(room_id = ?space.room_id(), "Could not deserialize m.space.parent: {e}");
None
}
}).for_each(|parent| graph.add_edge(parent, space.room_id().to_owned()));
} else {
error!(room_id = ?space.room_id(), "Could not get m.space.parent events");
}
if let Ok(children) = space.get_state_events_static::<SpaceChildEventContent>().await {
children.into_iter()
.filter_map(|child_event| match child_event.deserialize() {
Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(e))) => {
space_child_states.insert(
e.state_key.to_owned(),
SpaceRoomChildState {
order: e.content.order.clone(),
origin_server_ts: e.origin_server_ts,
},
);
Some(e.state_key)
}
Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => None,
Ok(SyncOrStrippedState::Stripped(e)) => Some(e.state_key),
Err(e) => {
trace!(room_id = ?space.room_id(), "Could not deserialize m.space.child: {e}");
None
}
}).for_each(|child| graph.add_edge(space.room_id().to_owned(), child));
} else {
error!(room_id = ?space.room_id(), "Could not get m.space.child events");
}
}
graph.remove_cycles();
let root_nodes = graph.root_nodes();
let top_level_space_rooms = joined_spaces
.iter()
.filter(|room| root_nodes.contains(&room.room_id()))
.collect::<Vec<_>>();
let mut top_level_space_order = HashMap::new();
for space in &top_level_space_rooms {
if let Ok(Some(raw_event)) =
space.account_data_static::<events::space_order::SpaceOrderEventContent>().await
&& let Ok(event) = raw_event.deserialize()
{
top_level_space_order.insert(space.room_id().to_owned(), event.content.order);
}
}
let top_level_space_rooms = top_level_space_rooms
.into_iter()
.sorted_by(|a, b| {
let a = (a.room_id(), top_level_space_order.get(a.room_id()).map(AsRef::as_ref));
let b = (b.room_id(), top_level_space_order.get(b.room_id()).map(AsRef::as_ref));
compare_top_level_space_rooms(a, b)
})
.collect::<Vec<_>>();
let top_level_spaces = top_level_space_rooms
.iter()
.map(|room| {
SpaceRoom::new_from_known(room, graph.children_of(room.room_id()).len() as u64)
})
.collect();
let space_filters =
Self::build_space_filters(client, &graph, top_level_space_rooms, space_child_states);
(top_level_spaces, space_filters, graph)
}
fn build_space_filters(
client: &Client,
graph: &SpaceGraph,
top_level_space_rooms: Vec<&Room>,
space_child_states: HashMap<OwnedRoomId, SpaceRoomChildState>,
) -> Vec<SpaceFilter> {
let mut filters = Vec::new();
for top_level_space in top_level_space_rooms {
let children = graph
.children_of(top_level_space.room_id())
.into_iter()
.map(|id| id.to_owned())
.collect::<Vec<_>>();
filters.push(SpaceFilter {
space_room: SpaceRoom::new_from_known(top_level_space, children.len() as u64),
level: 0,
descendants: children.clone(),
});
filters.append(
&mut children
.iter()
.filter_map(|id| client.get_room(id))
.filter(|room| room.is_space())
.map(|room| {
SpaceRoom::new_from_known(
&room,
graph.children_of(room.room_id()).len() as u64,
)
})
.sorted_by(|a, b| {
let a_state = space_child_states.get(&a.room_id).cloned();
let b_state = space_child_states.get(&b.room_id).cloned();
SpaceRoom::compare_rooms(
(&a.room_id, a_state.as_ref()),
(&b.room_id, b_state.as_ref()),
)
})
.map(|space_room| {
let descendants = graph.flattened_bottom_up_subtree(&space_room.room_id);
SpaceFilter { space_room, level: 1, descendants }
})
.collect::<Vec<_>>(),
);
}
filters
}
}
fn compare_top_level_space_rooms(
a: (&RoomId, Option<&SpaceChildOrder>),
b: (&RoomId, Option<&SpaceChildOrder>),
) -> Ordering {
let (a_room_id, a_order) = a;
let (b_room_id, b_order) = b;
match (a_order, b_order) {
(Some(a_order), Some(b_order)) => a_order.cmp(b_order).then(a_room_id.cmp(b_room_id)),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => a_room_id.cmp(b_room_id),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpaceFilter {
pub space_room: SpaceRoom,
pub level: u8,
pub descendants: Vec<OwnedRoomId>,
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use assert_matches2::assert_let;
use eyeball_im::VectorDiff;
use futures_util::{StreamExt, pin_mut};
use matrix_sdk::{room::ParentSpace, test_utils::mocks::MatrixMockServer};
use matrix_sdk_test::{
JoinedRoomBuilder, LeftRoomBuilder, async_test, event_factory::EventFactory,
};
use proptest::prelude::*;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedSpaceChildOrder, RoomVersionId, UserId, event_id,
owned_room_id, room_id, serde::Raw,
};
use serde_json::json;
use stream_assert::{assert_next_eq, assert_pending};
use super::*;
#[async_test]
async fn test_spaces_hierarchy() {
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();
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");
add_space_rooms(
vec![
MockSpaceRoomParameters {
room_id: child_space_id_1,
order: None,
parents: vec![parent_space_id],
children: vec![],
power_level: None,
},
MockSpaceRoomParameters {
room_id: child_space_id_2,
order: None,
parents: vec![parent_space_id],
children: vec![],
power_level: None,
},
MockSpaceRoomParameters {
room_id: parent_space_id,
order: None,
parents: vec![],
children: vec![child_space_id_1, child_space_id_2],
power_level: None,
},
],
&client,
&server,
&factory,
user_id,
)
.await;
assert_eq!(
space_service
.top_level_joined_spaces()
.await
.iter()
.map(|s| s.room_id.to_owned())
.collect::<Vec<_>>(),
vec![parent_space_id]
);
assert_eq!(
space_service
.top_level_joined_spaces()
.await
.iter()
.map(|s| s.children_count)
.collect::<Vec<_>>(),
vec![2]
);
let parent_space = client.get_room(parent_space_id).unwrap();
assert!(parent_space.is_space());
let spaces: Vec<ParentSpace> = client
.get_room(child_space_id_1)
.unwrap()
.parent_spaces()
.await
.unwrap()
.map(Result::unwrap)
.collect()
.await;
assert_let!(ParentSpace::Reciprocal(parent) = spaces.first().unwrap());
assert_eq!(parent.room_id(), parent_space.room_id());
let spaces: Vec<ParentSpace> = client
.get_room(child_space_id_2)
.unwrap()
.parent_spaces()
.await
.unwrap()
.map(Result::unwrap)
.collect()
.await;
assert_let!(ParentSpace::Reciprocal(parent) = spaces.last().unwrap());
assert_eq!(parent.room_id(), parent_space.room_id());
}
#[async_test]
async fn test_joined_spaces_updates() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
let first_space_id = room_id!("!first_space:example.org");
let second_space_id = room_id!("!second_space:example.org");
server
.sync_room(
&client,
JoinedRoomBuilder::new(first_space_id)
.add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type()),
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let (initial_values, joined_spaces_subscriber) =
space_service.subscribe_to_top_level_joined_spaces().await;
pin_mut!(joined_spaces_subscriber);
assert_pending!(joined_spaces_subscriber);
assert_eq!(
initial_values,
vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)].into()
);
assert_eq!(
space_service.top_level_joined_spaces().await,
vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)]
);
assert_pending!(joined_spaces_subscriber);
server
.sync_room(
&client,
JoinedRoomBuilder::new(second_space_id)
.add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type())
.add_state_event(
factory
.space_child(
second_space_id.to_owned(),
owned_room_id!("!child:example.org"),
)
.sender(user_id),
),
)
.await;
assert_eq!(
space_service.top_level_joined_spaces().await,
vec![
SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0),
SpaceRoom::new_from_known(&client.get_room(second_space_id).unwrap(), 1)
]
);
assert_next_eq!(
joined_spaces_subscriber,
vec![
VectorDiff::Clear,
VectorDiff::Append {
values: vec![
SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0),
SpaceRoom::new_from_known(&client.get_room(second_space_id).unwrap(), 1)
]
.into()
},
]
);
server.sync_room(&client, LeftRoomBuilder::new(second_space_id)).await;
assert_next_eq!(
joined_spaces_subscriber,
vec![
VectorDiff::Clear,
VectorDiff::Append {
values: vec![SpaceRoom::new_from_known(
&client.get_room(first_space_id).unwrap(),
0
)]
.into()
},
]
);
server
.sync_room(
&client,
JoinedRoomBuilder::new(room_id!("!room:example.org"))
.add_state_event(factory.create(user_id, RoomVersionId::V1)),
)
.await;
assert_pending!(joined_spaces_subscriber);
assert_eq!(
space_service.top_level_joined_spaces().await,
vec![SpaceRoom::new_from_known(&client.get_room(first_space_id).unwrap(), 0)]
);
}
#[async_test]
async fn test_space_filters() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
server.mock_room_state_encryption().plain().mount().await;
add_space_rooms(
vec![
MockSpaceRoomParameters {
room_id: room_id!("!1:a.b"),
order: None,
parents: vec![],
children: vec![],
power_level: None,
},
MockSpaceRoomParameters {
room_id: room_id!("!1.2:a.b"),
order: None,
parents: vec![room_id!("!1:a.b")],
children: vec![],
power_level: None,
},
MockSpaceRoomParameters {
room_id: room_id!("!1.2.3:a.b"),
order: None,
parents: vec![room_id!("!1.2:a.b")],
children: vec![],
power_level: None,
},
MockSpaceRoomParameters {
room_id: room_id!("!1.2.3.4:a.b"),
order: None,
parents: vec![room_id!("!1.2.3:a.b")],
children: vec![],
power_level: None,
},
],
&client,
&server,
&EventFactory::new(),
client.user_id().unwrap(),
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let filters = space_service.space_filters().await;
assert_eq!(filters.len(), 2);
assert_eq!(filters[0].space_room.room_id, room_id!("!1:a.b"));
assert_eq!(filters[0].level, 0);
assert_eq!(filters[0].descendants.len(), 1); assert_eq!(filters[1].space_room.room_id, room_id!("!1.2:a.b"));
assert_eq!(filters[1].level, 1);
assert_eq!(filters[1].descendants.len(), 3);
let (initial_values, space_filters_subscriber) =
space_service.subscribe_to_space_filters().await;
pin_mut!(space_filters_subscriber);
assert_pending!(space_filters_subscriber);
assert_eq!(initial_values, filters.into());
add_space_rooms(
vec![MockSpaceRoomParameters {
room_id: room_id!("!1.2.3.4.5:a.b"),
order: None,
parents: vec![room_id!("!1.2.3.4:a.b")],
children: vec![],
power_level: None,
}],
&client,
&server,
&EventFactory::new(),
client.user_id().unwrap(),
)
.await;
space_filters_subscriber.next().await;
let filters = space_service.space_filters().await;
assert_eq!(filters[0].descendants.len(), 1);
assert_eq!(filters[1].descendants.len(), 4);
}
#[async_test]
async fn test_top_level_space_order() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
server.mock_room_state_encryption().plain().mount().await;
add_space_rooms(
vec![
MockSpaceRoomParameters {
room_id: room_id!("!2:a.b"),
order: Some("2"),
parents: vec![],
children: vec![],
power_level: None,
},
MockSpaceRoomParameters {
room_id: room_id!("!4:a.b"),
order: None,
parents: vec![],
children: vec![],
power_level: None,
},
MockSpaceRoomParameters {
room_id: room_id!("!3:a.b"),
order: None,
parents: vec![],
children: vec![],
power_level: None,
},
MockSpaceRoomParameters {
room_id: room_id!("!1:a.b"),
order: Some("1"),
parents: vec![],
children: vec![],
power_level: None,
},
],
&client,
&server,
&EventFactory::new(),
client.user_id().unwrap(),
)
.await;
let space_service = SpaceService::new(client.clone()).await;
assert_eq!(
space_service.top_level_joined_spaces().await,
vec![
SpaceRoom::new_from_known(&client.get_room(room_id!("!1:a.b")).unwrap(), 0),
SpaceRoom::new_from_known(&client.get_room(room_id!("!2:a.b")).unwrap(), 0),
SpaceRoom::new_from_known(&client.get_room(room_id!("!3:a.b")).unwrap(), 0),
SpaceRoom::new_from_known(&client.get_room(room_id!("!4:a.b")).unwrap(), 0),
]
);
}
#[async_test]
async fn test_editable_spaces() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
let admin_space_id = room_id!("!admin_space:example.org");
let admin_subspace_id = room_id!("!admin_subspace:example.org");
let regular_space_id = room_id!("!regular_space:example.org");
let regular_subspace_id = room_id!("!regular_subspace:example.org");
add_space_rooms(
vec![
MockSpaceRoomParameters {
room_id: admin_space_id,
order: None,
parents: vec![],
children: vec![regular_subspace_id],
power_level: Some(100),
},
MockSpaceRoomParameters {
room_id: admin_subspace_id,
order: None,
parents: vec![regular_space_id],
children: vec![],
power_level: Some(100),
},
MockSpaceRoomParameters {
room_id: regular_space_id,
order: None,
parents: vec![],
children: vec![admin_subspace_id],
power_level: Some(0),
},
MockSpaceRoomParameters {
room_id: regular_subspace_id,
order: None,
parents: vec![admin_space_id],
children: vec![],
power_level: Some(0),
},
],
&client,
&server,
&factory,
user_id,
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let editable_spaces = space_service.editable_spaces().await;
assert_eq!(
editable_spaces.iter().map(|room| room.room_id.to_owned()).collect::<Vec<_>>(),
vec![admin_space_id.to_owned(), admin_subspace_id.to_owned()]
);
}
#[async_test]
async fn test_joined_parents_of_child() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
let parent_space_id_1 = room_id!("!parent_space_1:example.org");
let parent_space_id_2 = room_id!("!parent_space_2:example.org");
let unknown_parent_space_id = room_id!("!unknown_parent_space:example.org");
let child_space_id = room_id!("!child_space:example.org");
add_space_rooms(
vec![
MockSpaceRoomParameters {
room_id: child_space_id,
order: None,
parents: vec![parent_space_id_1, parent_space_id_2, unknown_parent_space_id],
children: vec![],
power_level: None,
},
MockSpaceRoomParameters {
room_id: parent_space_id_1,
order: None,
parents: vec![],
children: vec![child_space_id],
power_level: None,
},
MockSpaceRoomParameters {
room_id: parent_space_id_2,
order: None,
parents: vec![],
children: vec![child_space_id],
power_level: None,
},
],
&client,
&server,
&factory,
user_id,
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let parents = space_service.joined_parents_of_child(child_space_id).await;
assert_eq!(
parents.iter().map(|space| space.room_id.to_owned()).collect::<Vec<_>>(),
vec![parent_space_id_1, parent_space_id_2]
);
}
#[async_test]
async fn test_get_space_room_for_id() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
let space_id = room_id!("!single_space:example.org");
add_space_rooms(
vec![MockSpaceRoomParameters {
room_id: space_id,
order: None,
parents: vec![],
children: vec![],
power_level: None,
}],
&client,
&server,
&factory,
user_id,
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let found = space_service.get_space_room(space_id).await;
assert!(found.is_some());
let expected = SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0);
assert_eq!(found.unwrap(), expected);
}
#[async_test]
async fn test_add_child_to_space() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
let space_child_event_id = event_id!("$1");
let space_parent_event_id = event_id!("$2");
server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
let space_id = room_id!("!my_space:example.org");
let child_id = room_id!("!my_child:example.org");
add_space_rooms(
vec![
MockSpaceRoomParameters {
room_id: space_id,
order: None,
parents: vec![],
children: vec![],
power_level: Some(100),
},
MockSpaceRoomParameters {
room_id: child_id,
order: None,
parents: vec![],
children: vec![],
power_level: Some(100),
},
],
&client,
&server,
&factory,
user_id,
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let result =
space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
assert!(result.is_ok());
}
#[async_test]
async fn test_add_child_to_space_without_space_admin() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
server.mock_set_space_child().unauthorized().expect(1).mount().await;
server.mock_set_space_parent().unauthorized().expect(0).mount().await;
let space_id = room_id!("!my_space:example.org");
let child_id = room_id!("!my_child:example.org");
add_space_rooms(
vec![
MockSpaceRoomParameters {
room_id: space_id,
order: None,
parents: vec![],
children: vec![],
power_level: Some(0),
},
MockSpaceRoomParameters {
room_id: child_id,
order: None,
parents: vec![],
children: vec![],
power_level: Some(0),
},
],
&client,
&server,
&factory,
user_id,
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let result =
space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
assert!(result.is_err());
}
#[async_test]
async fn test_add_child_to_space_without_child_admin() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
let space_child_event_id = event_id!("$1");
server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
server.mock_set_space_parent().unauthorized().expect(0).mount().await;
let space_id = room_id!("!my_space:example.org");
let child_id = room_id!("!my_child:example.org");
add_space_rooms(
vec![
MockSpaceRoomParameters {
room_id: space_id,
order: None,
parents: vec![],
children: vec![],
power_level: Some(100),
},
MockSpaceRoomParameters {
room_id: child_id,
order: None,
parents: vec![],
children: vec![],
power_level: Some(0),
},
],
&client,
&server,
&factory,
user_id,
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let result =
space_service.add_child_to_space(child_id.to_owned(), space_id.to_owned()).await;
error!("result: {:?}", result);
assert!(result.is_ok());
}
#[async_test]
async fn test_remove_child_from_space() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
let space_child_event_id = event_id!("$1");
let space_parent_event_id = event_id!("$2");
server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
let parent_id = room_id!("!parent_space:example.org");
let child_id = room_id!("!child_space:example.org");
add_space_rooms(
vec![
MockSpaceRoomParameters {
room_id: parent_id,
order: None,
parents: vec![],
children: vec![child_id],
power_level: None,
},
MockSpaceRoomParameters {
room_id: child_id,
order: None,
parents: vec![parent_id],
children: vec![],
power_level: None,
},
],
&client,
&server,
&factory,
user_id,
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let result =
space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
assert!(result.is_ok());
}
#[async_test]
async fn test_remove_child_from_space_without_parent_event() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
let space_child_event_id = event_id!("$1");
server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
server.mock_set_space_parent().unauthorized().expect(0).mount().await;
let parent_id = room_id!("!parent_space:example.org");
let child_id = room_id!("!child_space:example.org");
add_space_rooms(
vec![
MockSpaceRoomParameters {
room_id: parent_id,
order: None,
parents: vec![],
children: vec![child_id],
power_level: None,
},
MockSpaceRoomParameters {
room_id: child_id,
order: None,
parents: vec![],
children: vec![],
power_level: None,
},
],
&client,
&server,
&factory,
user_id,
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let result =
space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
assert!(result.is_ok());
}
#[async_test]
async fn test_remove_child_from_space_without_child_event() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
let space_parent_event_id = event_id!("$2");
server.mock_set_space_child().unauthorized().expect(0).mount().await;
server.mock_set_space_parent().ok(space_parent_event_id.to_owned()).expect(1).mount().await;
let parent_id = room_id!("!parent_space:example.org");
let child_id = room_id!("!child_space:example.org");
add_space_rooms(
vec![
MockSpaceRoomParameters {
room_id: parent_id,
order: None,
parents: vec![],
children: vec![],
power_level: None,
},
MockSpaceRoomParameters {
room_id: child_id,
order: None,
parents: vec![parent_id],
children: vec![],
power_level: None,
},
],
&client,
&server,
&factory,
user_id,
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let result =
space_service.remove_child_from_space(child_id.to_owned(), parent_id.to_owned()).await;
assert!(result.is_ok());
}
#[async_test]
async fn test_remove_unknown_child_from_space() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
let space_child_event_id = event_id!("$1");
server.mock_set_space_child().ok(space_child_event_id.to_owned()).expect(1).mount().await;
server.mock_set_space_parent().unauthorized().expect(0).mount().await;
let parent_id = room_id!("!parent_space:example.org");
let unknown_child_id = room_id!("!unknown_child:example.org");
add_space_rooms(
vec![MockSpaceRoomParameters {
room_id: parent_id,
order: None,
parents: vec![],
children: vec![unknown_child_id],
power_level: None,
}],
&client,
&server,
&factory,
user_id,
)
.await;
assert!(client.get_room(unknown_child_id).is_none());
let space_service = SpaceService::new(client.clone()).await;
let result = space_service
.remove_child_from_space(unknown_child_id.to_owned(), parent_id.to_owned())
.await;
assert!(result.is_ok());
}
#[async_test]
async fn test_space_child_updates() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let user_id = client.user_id().unwrap();
let factory = EventFactory::new();
server.mock_room_state_encryption().plain().mount().await;
let space_id = room_id!("!space:localhost");
let first_child_id = room_id!("!first_child:localhost");
let second_child_id = room_id!("!second_child:localhost");
server
.sync_room(
&client,
JoinedRoomBuilder::new(space_id)
.add_state_event(factory.create(user_id, RoomVersionId::V11).with_space_type()),
)
.await;
let space_service = SpaceService::new(client.clone()).await;
let (initial_values, joined_spaces_subscriber) =
space_service.subscribe_to_top_level_joined_spaces().await;
pin_mut!(joined_spaces_subscriber);
assert_pending!(joined_spaces_subscriber);
assert_eq!(
initial_values,
vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0)].into()
);
assert_eq!(
space_service.top_level_joined_spaces().await,
vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 0)]
);
server
.sync_room(
&client,
JoinedRoomBuilder::new(space_id)
.add_state_event(
factory
.space_child(space_id.to_owned(), first_child_id.to_owned())
.sender(user_id),
)
.add_state_event(
factory
.space_child(space_id.to_owned(), second_child_id.to_owned())
.sender(user_id),
),
)
.await;
assert_eq!(
space_service.top_level_joined_spaces().await,
vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 2)]
);
assert_next_eq!(
joined_spaces_subscriber,
vec![
VectorDiff::Clear,
VectorDiff::Append {
values: vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 2)]
.into()
},
]
);
server
.sync_room(
&client,
JoinedRoomBuilder::new(space_id).add_state_bulk([Raw::new(&json!({
"content": {},
"type": "m.space.child",
"event_id": "$cancelsecondchild",
"origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
"sender": user_id,
"state_key": second_child_id,
}))
.unwrap()
.cast_unchecked()]),
)
.await;
assert_eq!(
space_service.top_level_joined_spaces().await,
vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 1)]
);
assert_next_eq!(
joined_spaces_subscriber,
vec![
VectorDiff::Clear,
VectorDiff::Append {
values: vec![SpaceRoom::new_from_known(&client.get_room(space_id).unwrap(), 1)]
.into()
},
]
);
}
async fn add_space_rooms(
rooms: Vec<MockSpaceRoomParameters>,
client: &Client,
server: &MatrixMockServer,
factory: &EventFactory,
user_id: &UserId,
) {
for parameters in rooms {
let mut builder = JoinedRoomBuilder::new(parameters.room_id)
.add_state_event(factory.create(user_id, RoomVersionId::V1).with_space_type());
if let Some(order) = parameters.order {
builder = builder.add_account_data(factory.space_order(order));
}
for parent_id in parameters.parents {
builder = builder.add_state_event(
factory
.space_parent(parent_id.to_owned(), parameters.room_id.to_owned())
.sender(user_id),
);
}
for child_id in parameters.children {
builder = builder.add_state_event(
factory
.space_child(parameters.room_id.to_owned(), child_id.to_owned())
.sender(user_id),
);
}
let mut power_levels = if let Some(power_level) = parameters.power_level {
BTreeMap::from([(user_id.to_owned(), power_level.into())])
} else {
BTreeMap::from([(user_id.to_owned(), 100.into())])
};
builder = builder.add_state_event(
factory.power_levels(&mut power_levels).state_key("").sender(user_id),
);
server.sync_room(client, builder).await;
}
}
struct MockSpaceRoomParameters {
room_id: &'static RoomId,
order: Option<&'static str>,
parents: Vec<&'static RoomId>,
children: Vec<&'static RoomId>,
power_level: Option<i32>,
}
fn any_room_id_and_space_room_order()
-> impl Strategy<Value = (OwnedRoomId, Option<OwnedSpaceChildOrder>)> {
let room_id = "[a-zA-Z]{1,5}".prop_map(|r| {
RoomId::new_v2(&r).expect("Any string starting with ! should be a valid room ID")
});
let order = prop::option::of("[a-zA-Z]{1,5}").prop_map(|order| {
order.map(|o| SpaceChildOrder::parse(o).expect("Any string should be a valid order"))
});
(room_id, order)
}
proptest! {
#[test]
fn sort_top_level_space_room_never_panics(mut v in prop::collection::vec(any_room_id_and_space_room_order(), 0..100)) {
v.sort_by(|a, b| {
let (a_room_id, a_order) = a;
let (b_room_id, b_order) = b;
let a = (a_room_id.as_ref(), a_order.as_deref());
let b = (b_room_id.as_ref(), b_order.as_deref());
compare_top_level_space_rooms(a, b)
})
}
#[test]
fn test_compare_top_level_rooms_reflexive(a in any_room_id_and_space_room_order()) {
let (a_room_id, a_order) = a;
let a = (a_room_id.as_ref(), a_order.as_deref());
prop_assert_eq!(compare_top_level_space_rooms(a, a), Ordering::Equal);
}
#[test]
fn test_compare_top_level_rooms_antisymmetric(a in any_room_id_and_space_room_order(), b in any_room_id_and_space_room_order()) {
let (a_room_id, a_order) = a;
let (b_room_id, b_order) = b;
let a = (a_room_id.as_ref(), a_order.as_deref());
let b = (b_room_id.as_ref(), b_order.as_deref());
let ab = compare_top_level_space_rooms(a, b);
let ba = compare_top_level_space_rooms(b, a);
prop_assert_eq!(ab, ba.reverse());
}
#[test]
fn test_compare_top_level_rooms_transitive(
a in any_room_id_and_space_room_order(),
b in any_room_id_and_space_room_order(),
c in any_room_id_and_space_room_order()
) {
let (a_room_id, a_order) = a;
let (b_room_id, b_order) = b;
let (c_room_id, c_order) = c;
let a = (a_room_id.as_ref(), a_order.as_deref());
let b = (b_room_id.as_ref(), b_order.as_deref());
let c = (c_room_id.as_ref(), c_order.as_deref());
let ab = compare_top_level_space_rooms(a, b);
let bc = compare_top_level_space_rooms(b, c);
let ac = compare_top_level_space_rooms(a, c);
if ab == Ordering::Less && bc == Ordering::Less {
prop_assert_eq!(ac, Ordering::Less);
}
if ab == Ordering::Equal && bc == Ordering::Equal {
prop_assert_eq!(ac, Ordering::Equal);
}
if ab == Ordering::Greater && bc == Ordering::Greater {
prop_assert_eq!(ac, Ordering::Greater);
}
}
}
}