#[cfg(feature = "e2e-encryption")]
use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
use matrix_sdk_common::timer;
use ruma::{
OwnedRoomId, api::client::sync::sync_events::v5 as http, events::receipt::SyncReceiptEvent,
serde::Raw,
};
use tokio::sync::MutexGuard;
use tracing::{instrument, trace};
use super::BaseClient;
use crate::{
RequestedRequiredStates,
error::Result,
response_processors as processors,
store::ambiguity_map::AmbiguityCache,
sync::{RoomUpdates, SyncResponse},
};
impl BaseClient {
#[cfg(feature = "e2e-encryption")]
pub async fn process_sliding_sync_e2ee(
&self,
to_device: Option<&http::response::ToDevice>,
e2ee: &http::response::E2EE,
state_store_guard: &MutexGuard<'_, ()>,
) -> Result<Option<Vec<ProcessedToDeviceEvent>>> {
if to_device.is_none() && e2ee.is_empty() {
return Ok(None);
}
trace!(
to_device_events =
to_device.map(|to_device| to_device.events.len()).unwrap_or_default(),
device_one_time_keys_count = e2ee.device_one_time_keys_count.len(),
device_unused_fallback_key_types =
e2ee.device_unused_fallback_key_types.as_ref().map(|v| v.len()),
"Processing sliding sync e2ee events",
);
let olm_machine = self.olm_machine().await;
let context = processors::Context::default();
let processors::e2ee::to_device::Output { processed_to_device_events } =
processors::e2ee::to_device::from_msc4186(
to_device,
e2ee,
olm_machine.as_ref(),
&self.decryption_settings,
)
.await?;
processors::changes::save_and_apply(
context,
&self.state_store,
state_store_guard,
&self.ignore_user_list_changes,
None,
)
.await?;
Ok(Some(processed_to_device_events))
}
#[instrument(skip_all, level = "trace")]
pub async fn process_sliding_sync(
&self,
response: &http::Response,
requested_required_states: &RequestedRequiredStates,
state_store_guard: &MutexGuard<'_, ()>,
) -> Result<SyncResponse> {
let http::Response { rooms, lists, extensions, .. } = response;
trace!(
rooms = rooms.len(),
lists = lists.len(),
has_extensions = !extensions.is_empty(),
"Processing sliding sync room events"
);
if rooms.is_empty() && extensions.is_empty() {
return Ok(SyncResponse::default());
}
let _timer = timer!(tracing::Level::TRACE, "_method");
let mut context = processors::Context::default();
let state_store = self.state_store.clone();
let mut ambiguity_cache = AmbiguityCache::new(state_store.inner.clone());
let global_account_data_processor =
processors::account_data::global(&extensions.account_data.global);
let push_rules = self.get_push_rules(&global_account_data_processor).await?;
let mut room_updates = RoomUpdates::default();
let mut notifications = Default::default();
let user_id = self
.session_meta()
.expect("Sliding sync shouldn't run without an authenticated user")
.user_id
.to_owned();
for (room_id, room_response) in rooms {
let Some((room_info, room_update)) = processors::room::msc4186::update_any_room(
&mut context,
&user_id,
processors::room::RoomCreationData::new(
room_id,
requested_required_states,
&mut ambiguity_cache,
),
room_response,
&extensions.account_data.rooms,
#[cfg(feature = "e2e-encryption")]
processors::e2ee::E2EE::new(
self.olm_machine().await.as_ref(),
&self.decryption_settings,
self.handle_verification_events,
),
processors::notification::Notification::new(
&push_rules,
&mut notifications,
&self.state_store,
),
)
.await?
else {
continue;
};
context.state_changes.add_room(room_info);
let room_id = room_id.to_owned();
use processors::room::msc4186::RoomUpdateKind;
match room_update {
RoomUpdateKind::Joined(joined_room_update) => {
room_updates.joined.insert(room_id, joined_room_update);
}
RoomUpdateKind::Left(left_room_update) => {
room_updates.left.insert(room_id, left_room_update);
}
RoomUpdateKind::Invited(invited_room_update) => {
room_updates.invited.insert(room_id, invited_room_update);
}
RoomUpdateKind::Knocked(knocked_room_update) => {
room_updates.knocked.insert(room_id, knocked_room_update);
}
}
}
processors::room::msc4186::extensions::dispatch_typing_ephemeral_events(
&extensions.typing,
&mut room_updates.joined,
);
processors::room::msc4186::extensions::room_account_data(
&mut context,
&extensions.account_data,
&mut room_updates,
&self.state_store,
);
global_account_data_processor.apply(&mut context, &state_store).await;
context.state_changes.ambiguity_maps = ambiguity_cache.cache;
processors::changes::save_and_apply(
context,
&self.state_store,
state_store_guard,
&self.ignore_user_list_changes,
None,
)
.await?;
let mut context = processors::Context::default();
processors::room::display_name::update_for_rooms(
&mut context,
&room_updates,
&self.state_store,
)
.await;
processors::changes::save_only(context, &self.state_store, state_store_guard).await?;
Ok(SyncResponse {
rooms: room_updates,
notifications,
presence: Default::default(),
account_data: extensions.account_data.global.clone(),
to_device: Default::default(),
})
}
#[doc(hidden)]
pub async fn process_sliding_sync_receipts_extension_for_room(
&self,
room_id: &OwnedRoomId,
response: &http::Response,
state_store_guard: &MutexGuard<'_, ()>,
) -> Result<Option<Raw<SyncReceiptEvent>>> {
let mut context = processors::Context::default();
let mut save_context = false;
let receipt_ephemeral_event = if let Some(receipt_ephemeral_event) =
response.extensions.receipts.rooms.get(room_id)
{
processors::room::msc4186::extensions::dispatch_receipt_ephemeral_event_for_room(
&mut context,
room_id,
receipt_ephemeral_event,
);
save_context = true;
Some(receipt_ephemeral_event.clone())
} else {
None
};
if save_context {
processors::changes::save_only(context, &self.state_store, state_store_guard).await?;
}
Ok(receipt_ephemeral_event)
}
}
#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use std::collections::{BTreeMap, HashSet};
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use ruma::{
JsOption, MxcUri, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, UserId,
api::client::sync::sync_events::UnreadNotificationsCount,
assign, event_id,
events::{
GlobalAccountDataEventContent, StateEventContent, StateEventType,
direct::{DirectEventContent, DirectUserIdentifier, OwnedDirectUserIdentifier},
room::{
avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
encryption::RoomEncryptionEventContent,
member::{MembershipState, RoomMemberEventContent},
name::RoomNameEventContent,
pinned_events::RoomPinnedEventsEventContent,
},
},
mxc_uri, owned_event_id, owned_mxc_uri, owned_user_id, room_alias_id, room_id,
serde::Raw,
uint, user_id,
};
use serde_json::json;
use super::http;
use crate::{
BaseClient, DmRoomDefinition, EncryptionState, RequestedRequiredStates,
RoomInfoNotableUpdate, RoomState, SessionMeta,
client::ThreadingSupport,
room::{RoomHero, RoomInfoNotableUpdateReasons},
store::{RoomLoadSettings, StoreConfig},
test_utils::logged_in_base_client,
};
#[async_test]
async fn test_invited_state_without_update_emits_invited_room() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!invite:e.uk");
let user_id = client.session_meta().unwrap().user_id.to_owned();
let mut room = http::response::Room::new();
room.invite_state = Some(invite_state_for(&user_id, MembershipState::Invite));
let response = response_with_room(room_id, room);
let sync_resp = client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.unwrap();
assert!(sync_resp.rooms.invited.contains_key(room_id));
}
use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
use ruma::events::AnyStrippedStateEvent;
fn invite_state_for(
user_id: &UserId,
membership: MembershipState,
) -> Vec<Raw<AnyStrippedStateEvent>> {
let content = RoomMemberEventContent::new(membership);
let raw: Raw<AnyStrippedStateEvent> = Raw::from_json_string(
serde_json::json!({
"type": "m.room.member",
"state_key": user_id,
"content": content,
})
.to_string(),
)
.unwrap();
vec![raw]
}
#[async_test]
async fn test_knocked_state_emits_invited_room() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!knock:e.uk");
let user_id = client.session_meta().unwrap().user_id.to_owned();
let mut room = http::response::Room::new();
room.invite_state = Some(invite_state_for(&user_id, MembershipState::Knock));
let response = response_with_room(room_id, room);
let sync_resp = client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.unwrap();
assert!(sync_resp.rooms.invited.contains_key(room_id));
}
#[async_test]
async fn test_notification_count_set() {
let client = logged_in_base_client(None).await;
let mut response = http::Response::new("42".to_owned());
let room_id = room_id!("!room:example.org");
let count = assign!(UnreadNotificationsCount::default(), {
highlight_count: Some(uint!(13)),
notification_count: Some(uint!(37)),
});
response.rooms.insert(
room_id.to_owned(),
assign!(http::response::Room::new(), {
unread_notifications: count.clone()
}),
);
let sync_response = client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let room = sync_response.rooms.joined.get(room_id).unwrap();
assert_eq!(room.unread_notifications, count.clone().into());
let room = client.get_room(room_id).expect("found room");
assert_eq!(room.unread_notification_counts(), count.into());
}
#[async_test]
async fn test_can_process_empty_sliding_sync_response() {
let client = logged_in_base_client(None).await;
let empty_response = http::Response::new("5".to_owned());
client
.process_sliding_sync(
&empty_response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
}
#[async_test]
async fn test_room_with_unspecified_state_is_added_to_client_and_joined_list() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let mut room = http::response::Room::new();
room.joined_count = Some(uint!(41));
let response = response_with_room(room_id, room);
let sync_resp = client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.room_id(), room_id);
assert_eq!(client_room.joined_members_count(), 41);
assert_eq!(client_room.state(), RoomState::Joined);
assert!(sync_resp.rooms.joined.contains_key(room_id));
assert!(!sync_resp.rooms.left.contains_key(room_id));
assert!(!sync_resp.rooms.invited.contains_key(room_id));
}
#[async_test]
async fn test_missing_room_name_event() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let mut room = http::response::Room::new();
room.name = Some("little room".to_owned());
let response = response_with_room(room_id, room);
let sync_resp = client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert!(client_room.name().is_none());
assert_eq!(
client_room.compute_display_name().await.unwrap().into_inner().to_string(),
"Empty Room"
);
assert_eq!(client_room.state(), RoomState::Joined);
assert!(sync_resp.rooms.joined.contains_key(room_id));
assert!(!sync_resp.rooms.left.contains_key(room_id));
assert!(!sync_resp.rooms.invited.contains_key(room_id));
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
}
#[async_test]
async fn test_room_name_event() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let mut room = http::response::Room::new();
room.name = Some("little room".to_owned());
set_room_name(&mut room, user_id!("@a:b.c"), "The Name".to_owned());
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.name().as_deref(), Some("The Name"));
assert_eq!(
client_room.compute_display_name().await.unwrap().into_inner().to_string(),
"The Name"
);
}
#[async_test]
async fn test_missing_invited_room_name_event() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@w:e.uk");
let inviter = user_id!("@john:mastodon.org");
let mut room = http::response::Room::new();
set_room_invited(&mut room, inviter, user_id);
room.name = Some("name from sliding sync response".to_owned());
let response = response_with_room(room_id, room);
let sync_resp = client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert!(client_room.name().is_none());
assert_eq!(client_room.compute_display_name().await.unwrap().into_inner().to_string(), "w");
assert_eq!(client_room.state(), RoomState::Invited);
assert!(!sync_resp.rooms.joined.contains_key(room_id));
assert!(!sync_resp.rooms.left.contains_key(room_id));
assert!(sync_resp.rooms.invited.contains_key(room_id));
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
}
#[async_test]
async fn test_invited_room_name_event() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@w:e.uk");
let inviter = user_id!("@john:mastodon.org");
let mut room = http::response::Room::new();
set_room_invited(&mut room, inviter, user_id);
room.name = Some("name from sliding sync response".to_owned());
set_room_name(&mut room, user_id!("@a:b.c"), "The Name".to_owned());
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.name().as_deref(), Some("The Name"));
assert_eq!(
client_room.compute_display_name().await.unwrap().into_inner().to_string(),
"The Name"
);
}
#[async_test]
async fn test_receiving_a_knocked_room_membership_event_creates_a_knocked_room() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = client.session_meta().unwrap().user_id.to_owned();
let mut room = http::response::Room::new();
set_room_knocked(&mut room, &user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.state(), RoomState::Knocked);
}
#[async_test]
async fn test_receiving_a_knocked_room_membership_event_with_wrong_state_key_creates_an_invited_room()
{
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@w:e.uk");
let mut room = http::response::Room::new();
set_room_knocked(&mut room, user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.state(), RoomState::Invited);
}
#[async_test]
async fn test_receiving_an_unknown_room_membership_event_in_invite_state_creates_an_invited_room()
{
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = client.session_meta().unwrap().user_id.to_owned();
let mut room = http::response::Room::new();
let event = Raw::new(&json!({
"type": "m.room.member",
"sender": user_id,
"content": {
"is_direct": true,
"membership": "join",
},
"state_key": user_id,
}))
.expect("Failed to make raw event")
.cast_unchecked();
room.invite_state = Some(vec![event]);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.state(), RoomState::Invited);
}
#[async_test]
async fn test_left_a_room_from_required_state_event() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
let mut room = http::response::Room::new();
set_room_joined(&mut room, user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
let mut room = http::response::Room::new();
set_room_left(&mut room, user_id);
let response = response_with_room(room_id, room);
let sync_resp = client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
assert!(!sync_resp.rooms.joined.contains_key(room_id));
assert!(sync_resp.rooms.left.contains_key(room_id));
assert!(!sync_resp.rooms.invited.contains_key(room_id));
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
}
#[async_test]
async fn test_kick_or_ban_updates_room_to_left() {
for membership in [MembershipState::Leave, MembershipState::Ban] {
let room_id = room_id!("!r:e.uk");
let user_a_id = user_id!("@a:e.uk");
let user_b_id = user_id!("@b:e.uk");
let client = logged_in_base_client(Some(user_a_id)).await;
let mut room = http::response::Room::new();
set_room_joined(&mut room, user_a_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
let mut room = http::response::Room::new();
room.required_state.push(make_state_event(
user_b_id,
user_a_id.as_str(),
RoomMemberEventContent::new(membership.clone()),
None,
));
let response = response_with_room(room_id, room);
let sync_resp = client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
match membership {
MembershipState::Leave => {
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
}
MembershipState::Ban => {
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Banned);
}
_ => panic!("Unexpected membership state found: {membership}"),
}
assert!(!sync_resp.rooms.joined.contains_key(room_id));
assert!(sync_resp.rooms.left.contains_key(room_id));
assert!(!sync_resp.rooms.invited.contains_key(room_id));
assert!(!sync_resp.rooms.knocked.contains_key(room_id));
}
}
#[async_test]
async fn test_left_a_room_from_timeline_state_event() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
let mut room = http::response::Room::new();
set_room_joined(&mut room, user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
let mut room = http::response::Room::new();
set_room_left_as_timeline_event(&mut room, user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
}
#[async_test]
async fn test_can_be_reinvited_to_a_left_room() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
let mut room = http::response::Room::new();
set_room_joined(&mut room, user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Joined);
let mut room = http::response::Room::new();
set_room_left(&mut room, user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
let mut room = http::response::Room::new();
set_room_invited(&mut room, user_id, user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
}
#[async_test]
async fn test_other_person_leaving_a_dm_is_reflected_in_their_membership_and_direct_targets() {
let room_id = room_id!("!r:e.uk");
let user_a_id = user_id!("@a:e.uk");
let user_b_id = user_id!("@b:e.uk");
let client = logged_in_base_client(None).await;
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await;
assert!(
direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
);
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Join);
update_room_membership(&client, room_id, user_b_id, MembershipState::Leave).await;
assert!(
direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
);
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave);
}
#[async_test]
async fn test_other_person_refusing_invite_to_a_dm_is_reflected_in_their_membership_and_direct_targets()
{
let room_id = room_id!("!r:e.uk");
let user_a_id = user_id!("@a:e.uk");
let user_b_id = user_id!("@b:e.uk");
let client = logged_in_base_client(None).await;
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await;
assert!(
direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
);
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Invite);
update_room_membership(&client, room_id, user_b_id, MembershipState::Leave).await;
assert!(
direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
);
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Leave);
}
#[async_test]
async fn test_members_count_in_a_dm_where_other_person_has_joined() {
let room_id = room_id!("!r:bar.org");
let user_a_id = user_id!("@a:bar.org");
let user_b_id = user_id!("@b:bar.org");
let client = logged_in_base_client(None).await;
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Join).await;
assert_eq!(membership(&client, room_id, user_a_id).await, MembershipState::Join);
assert!(
direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
);
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Join);
let room = client.get_room(room_id).unwrap();
assert_eq!(room.active_members_count(), 2);
assert_eq!(room.joined_members_count(), 2);
assert_eq!(room.invited_members_count(), 0);
}
#[async_test]
async fn test_members_count_in_a_dm_where_other_person_is_invited() {
let room_id = room_id!("!r:bar.org");
let user_a_id = user_id!("@a:bar.org");
let user_b_id = user_id!("@b:bar.org");
let client = logged_in_base_client(None).await;
create_dm(&client, room_id, user_a_id, user_b_id, MembershipState::Invite).await;
assert_eq!(membership(&client, room_id, user_a_id).await, MembershipState::Join);
assert!(
direct_targets(&client, room_id).contains(<&DirectUserIdentifier>::from(user_b_id))
);
assert_eq!(membership(&client, room_id, user_b_id).await, MembershipState::Invite);
let room = client.get_room(room_id).unwrap();
assert_eq!(room.active_members_count(), 2);
assert_eq!(room.joined_members_count(), 1);
assert_eq!(room.invited_members_count(), 1);
}
#[async_test]
async fn test_avatar_is_found_when_processing_sliding_sync_response() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let room = {
let mut room = http::response::Room::new();
room.avatar = JsOption::from_option(Some(owned_mxc_uri!("mxc://e.uk/med1")));
room
};
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(
client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
"med1"
);
}
#[async_test]
async fn test_avatar_can_be_unset_when_processing_sliding_sync_response() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let room = {
let mut room = http::response::Room::new();
room.avatar = JsOption::from_option(Some(owned_mxc_uri!("mxc://e.uk/med1")));
room
};
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(
client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
"med1"
);
let room = http::response::Room::new();
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(
client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
"med1"
);
let room = {
let mut room = http::response::Room::new();
room.avatar = JsOption::Null;
room
};
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert!(client_room.avatar_url().is_none());
}
#[async_test]
async fn test_avatar_is_found_from_required_state_when_processing_sliding_sync_response() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
let room = room_with_avatar(mxc_uri!("mxc://e.uk/med1"), user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(
client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
"med1"
);
}
#[async_test]
async fn test_invitation_room_is_added_to_client_and_invite_list() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
let mut room_info_notable_update = client.room_info_notable_update_receiver();
let mut room = http::response::Room::new();
set_room_invited(&mut room, user_id, user_id);
let response = response_with_room(room_id, room);
let sync_resp = client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.room_id(), room_id);
assert_eq!(client_room.state(), RoomState::Invited);
assert!(!sync_resp.rooms.invited[room_id].invite_state.is_empty());
assert!(!sync_resp.rooms.joined.contains_key(room_id));
assert_matches!(
room_info_notable_update.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP));
}
);
assert_matches!(
room_info_notable_update.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
}
);
assert!(room_info_notable_update.is_empty());
}
#[async_test]
async fn test_knock_room_is_added_to_client_and_knock_list() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
let mut room_info_notable_update = client.room_info_notable_update_receiver();
let mut room = http::response::Room::new();
set_room_knocked(&mut room, user_id);
let response = response_with_room(room_id, room);
let sync_resp = client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.room_id(), room_id);
assert_eq!(client_room.state(), RoomState::Knocked);
assert!(!sync_resp.rooms.knocked[room_id].knock_state.is_empty());
assert!(!sync_resp.rooms.joined.contains_key(room_id));
assert_matches!(
room_info_notable_update.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP));
}
);
assert_matches!(
room_info_notable_update.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
}
);
assert!(room_info_notable_update.is_empty());
}
#[async_test]
async fn test_avatar_is_found_in_invitation_room_when_processing_sliding_sync_response() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
let mut room = room_with_avatar(mxc_uri!("mxc://e.uk/med1"), user_id);
set_room_invited(&mut room, user_id, user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(
client_room.avatar_url().expect("No avatar URL").media_id().expect("No media ID"),
"med1"
);
}
#[async_test]
async fn test_canonical_alias_is_found_in_invitation_room_when_processing_sliding_sync_response()
{
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
let room_alias_id = room_alias_id!("#myroom:e.uk");
let mut room = room_with_canonical_alias(room_alias_id, user_id);
set_room_invited(&mut room, user_id, user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.canonical_alias(), Some(room_alias_id.to_owned()));
}
#[async_test]
async fn test_display_name_from_sliding_sync_doesnt_override_alias() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let user_id = user_id!("@u:e.uk");
let room_alias_id = room_alias_id!("#myroom:e.uk");
let mut room = room_with_canonical_alias(room_alias_id, user_id);
room.name = Some("This came from the server".to_owned());
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(
client_room.compute_display_name().await.unwrap().into_inner().to_string(),
"myroom"
);
assert!(client_room.name().is_none());
}
#[async_test]
async fn test_display_name_is_cached_and_emits_a_notable_update_reason() {
let client = logged_in_base_client(None).await;
let user_id = user_id!("@u:e.uk");
let room_id = room_id!("!r:e.uk");
let mut room_info_notable_update = client.room_info_notable_update_receiver();
let room = room_with_name("Hello World", user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let room = client.get_room(room_id).expect("No room found");
assert_eq!(room.cached_display_name().unwrap().to_string(), "Hello World");
assert_matches!(
room_info_notable_update.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(reasons.contains(RoomInfoNotableUpdateReasons::NONE));
}
);
assert_matches!(
room_info_notable_update.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
}
);
assert!(room_info_notable_update.is_empty());
}
#[async_test]
async fn test_display_name_is_persisted_from_sliding_sync() {
let user_id = user_id!("@u:e.uk");
let room_id = room_id!("!r:e.uk");
let session_meta = SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() };
let state_store;
{
let client = {
let store = StoreConfig::new(CrossProcessLockConfig::SingleProcess);
state_store = store.state_store.clone();
let client =
BaseClient::new(store, ThreadingSupport::Disabled, DmRoomDefinition::default());
client
.activate(
session_meta.clone(),
RoomLoadSettings::default(),
#[cfg(feature = "e2e-encryption")]
None,
)
.await
.expect("`activate` failed!");
client
};
let room = room_with_name("Hello World", user_id);
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let room = client.get_room(room_id).expect("No room found");
assert_eq!(room.cached_display_name().unwrap().to_string(), "Hello World");
}
{
let client = {
let mut store = StoreConfig::new(CrossProcessLockConfig::SingleProcess);
store.state_store = state_store;
let client =
BaseClient::new(store, ThreadingSupport::Disabled, DmRoomDefinition::default());
client
.activate(
session_meta,
RoomLoadSettings::default(),
#[cfg(feature = "e2e-encryption")]
None,
)
.await
.expect("`activate` failed!");
client
};
let room = client.get_room(room_id).expect("No room found");
assert_eq!(room.cached_display_name().unwrap().to_string(), "Hello World");
}
}
#[async_test]
async fn test_compute_heroes_from_sliding_sync() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let gordon = owned_user_id!("@gordon:e.uk");
let alice = owned_user_id!("@alice:e.uk");
let mut room = http::response::Room::new();
room.heroes = Some(vec![
assign!(http::response::Hero::new(gordon), {
name: Some("Gordon".to_owned()),
}),
assign!(http::response::Hero::new(alice), {
name: Some("Alice".to_owned()),
avatar: Some(owned_mxc_uri!("mxc://e.uk/med1"))
}),
]);
let response = response_with_room(room_id, room);
let _sync_resp = client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.room_id(), room_id);
assert_eq!(client_room.state(), RoomState::Joined);
assert_eq!(
client_room.clone_info().summary.heroes(),
&[
RoomHero {
user_id: owned_user_id!("@gordon:e.uk"),
display_name: Some("Gordon".to_owned()),
avatar_url: None
},
RoomHero {
user_id: owned_user_id!("@alice:e.uk"),
display_name: Some("Alice".to_owned()),
avatar_url: Some(owned_mxc_uri!("mxc://e.uk/med1"))
},
]
);
}
#[async_test]
async fn test_recency_stamp_is_found_when_processing_sliding_sync_response() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
let room = assign!(http::response::Room::new(), {
bump_stamp: Some(42u32.into()),
});
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42.into());
}
#[async_test]
async fn test_recency_stamp_can_be_overwritten_when_present_in_a_sliding_sync_response() {
let client = logged_in_base_client(None).await;
let room_id = room_id!("!r:e.uk");
{
let room = assign!(http::response::Room::new(), {
bump_stamp: Some(42u32.into()),
});
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42.into());
}
{
let room = assign!(http::response::Room::new(), {
bump_stamp: None,
});
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 42.into());
}
{
let room = assign!(http::response::Room::new(), {
bump_stamp: Some(153u32.into()),
});
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let client_room = client.get_room(room_id).expect("No room found");
assert_eq!(client_room.recency_stamp().expect("No recency stamp"), 153.into());
}
}
#[async_test]
async fn test_recency_stamp_can_trigger_a_notable_update_reason() {
let client = logged_in_base_client(None).await;
let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
let room_id = room_id!("!r:e.uk");
let room = assign!(http::response::Room::new(), {
bump_stamp: Some(42u32.into()),
});
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(!received_reasons.contains(RoomInfoNotableUpdateReasons::RECENCY_STAMP));
}
);
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
}
);
assert!(room_info_notable_update_stream.is_empty());
let room = assign!(http::response::Room::new(), {
bump_stamp: Some(43u32.into()),
});
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::RECENCY_STAMP));
}
);
assert!(room_info_notable_update_stream.is_empty());
}
#[async_test]
async fn test_leaving_room_can_trigger_a_notable_update_reason() {
let client = logged_in_base_client(None).await;
let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
let room_id = room_id!("!r:e.uk");
let room = http::response::Room::new();
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE));
}
);
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME));
}
);
let room_id = room_id!("!r:e.uk");
let events = vec![
Raw::from_json_string(
json!({
"type": "m.room.member",
"event_id": "$3",
"content": { "membership": "join" },
"sender": "@u:h.uk",
"origin_server_ts": 12344445,
"state_key": "@u:e.uk",
})
.to_string(),
)
.unwrap(),
];
let room = assign!(http::response::Room::new(), {
required_state: events,
});
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE));
}
);
assert!(room_info_notable_update_stream.is_empty());
let events = vec![
Raw::from_json_string(
json!({
"type": "m.room.member",
"event_id": "$3",
"content": { "membership": "leave" },
"sender": "@u:h.uk",
"origin_server_ts": 12344445,
"state_key": "@u:e.uk",
})
.to_string(),
)
.unwrap(),
];
let room = assign!(http::response::Room::new(), {
required_state: events,
});
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP));
}
);
assert!(room_info_notable_update_stream.is_empty());
}
#[async_test]
async fn test_unread_marker_can_trigger_a_notable_update_reason() {
let client = logged_in_base_client(None).await;
let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
let room_id = room_id!("!r:e.uk");
let room = http::response::Room::new();
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
}
);
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
}
);
assert!(room_info_notable_update_stream.is_empty());
let room_id = room_id!("!r:e.uk");
let room_account_data_events = vec![
Raw::from_json_string(
json!({
"type": "m.marked_unread",
"event_id": "$1",
"content": { "unread": true },
"sender": client.session_meta().unwrap().user_id,
"origin_server_ts": 12344445,
})
.to_string(),
)
.unwrap(),
];
let mut response = response_with_room(room_id, http::response::Room::new());
response.extensions.account_data.rooms.insert(room_id.to_owned(), room_account_data_events);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::UNREAD_MARKER), "{received_reasons:?}");
}
);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
}
);
assert!(room_info_notable_update_stream.is_empty());
let room_account_data_events = vec![
Raw::from_json_string(
json!({
"type": "m.marked_unread",
"event_id": "$1",
"content": { "unread": false },
"sender": client.session_meta().unwrap().user_id,
"origin_server_ts": 12344445,
})
.to_string(),
)
.unwrap(),
];
response.extensions.account_data.rooms.insert(room_id.to_owned(), room_account_data_events);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::UNREAD_MARKER));
}
);
assert!(room_info_notable_update_stream.is_empty());
}
#[async_test]
async fn test_unstable_unread_marker_is_ignored_after_stable() {
let client = logged_in_base_client(None).await;
let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
let room_id = room_id!("!r:e.uk");
let room = http::response::Room::new();
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
}
);
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
}
);
assert!(room_info_notable_update_stream.is_empty());
let room_id = room_id!("!r:e.uk");
let unstable_room_account_data_events = vec![
Raw::from_json_string(
json!({
"type": "com.famedly.marked_unread",
"event_id": "$1",
"content": { "unread": true },
"sender": client.session_meta().unwrap().user_id,
"origin_server_ts": 12344445,
})
.to_string(),
)
.unwrap(),
];
let mut response = response_with_room(room_id, http::response::Room::new());
response
.extensions
.account_data
.rooms
.insert(room_id.to_owned(), unstable_room_account_data_events.clone());
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::UNREAD_MARKER), "{received_reasons:?}");
}
);
assert!(room_info_notable_update_stream.is_empty());
let stable_room_account_data_events = vec![
Raw::from_json_string(
json!({
"type": "m.marked_unread",
"event_id": "$1",
"content": { "unread": false },
"sender": client.session_meta().unwrap().user_id,
"origin_server_ts": 12344445,
})
.to_string(),
)
.unwrap(),
];
response
.extensions
.account_data
.rooms
.insert(room_id.to_owned(), stable_room_account_data_events);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::UNREAD_MARKER));
}
);
assert!(room_info_notable_update_stream.is_empty());
response
.extensions
.account_data
.rooms
.insert(room_id.to_owned(), unstable_room_account_data_events);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
}
);
assert!(room_info_notable_update_stream.is_empty());
let stable_room_account_data_events = vec![
Raw::from_json_string(
json!({
"type": "m.marked_unread",
"event_id": "$3",
"content": { "unread": true },
"sender": client.session_meta().unwrap().user_id,
"origin_server_ts": 12344445,
})
.to_string(),
)
.unwrap(),
];
response
.extensions
.account_data
.rooms
.insert(room_id.to_owned(), stable_room_account_data_events);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
room_info_notable_update_stream.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::UNREAD_MARKER));
}
);
assert!(room_info_notable_update_stream.is_empty());
}
#[async_test]
async fn test_pinned_events_are_updated_on_sync() {
let user_a_id = user_id!("@a:e.uk");
let client = logged_in_base_client(Some(user_a_id)).await;
let room_id = room_id!("!r:e.uk");
let pinned_event_id = owned_event_id!("$an-id:e.uk");
let mut room_response = http::response::Room::new();
set_room_joined(&mut room_response, user_a_id);
let response = response_with_room(room_id, room_response);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let room = client.get_room(room_id).unwrap();
let pinned_event_ids = room.pinned_event_ids();
assert_matches!(pinned_event_ids, None);
let mut room_response = http::response::Room::new();
room_response.required_state.push(make_state_event(
user_a_id,
"",
RoomPinnedEventsEventContent::new(vec![pinned_event_id.clone()]),
None,
));
let response = response_with_room(room_id, room_response);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let pinned_event_ids = room.pinned_event_ids().unwrap_or_default();
assert_eq!(pinned_event_ids.len(), 1);
assert_eq!(pinned_event_ids[0], pinned_event_id);
let mut room_response = http::response::Room::new();
room_response.required_state.push(make_state_event(
user_a_id,
"",
RoomPinnedEventsEventContent::new(Vec::new()),
None,
));
let response = response_with_room(room_id, room_response);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let pinned_event_ids = room.pinned_event_ids().unwrap();
assert!(pinned_event_ids.is_empty());
}
#[async_test]
async fn test_dms_are_processed_in_any_sync_response() {
let current_user_id = user_id!("@current:e.uk");
let client = logged_in_base_client(Some(current_user_id)).await;
let user_a_id = user_id!("@a:e.uk");
let user_b_id = user_id!("@b:e.uk");
let room_id_1 = room_id!("!r:e.uk");
let room_id_2 = room_id!("!s:e.uk");
let mut room_response = http::response::Room::new();
set_room_joined(&mut room_response, user_a_id);
let mut response = response_with_room(room_id_1, room_response);
let mut direct_content: BTreeMap<OwnedDirectUserIdentifier, Vec<OwnedRoomId>> =
BTreeMap::new();
direct_content.insert(user_a_id.into(), vec![room_id_1.to_owned()]);
direct_content.insert(user_b_id.into(), vec![room_id_2.to_owned()]);
response
.extensions
.account_data
.global
.push(make_global_account_data_event(DirectEventContent(direct_content)));
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let room_1 = client.get_room(room_id_1).unwrap();
assert!(room_1.is_direct().await.unwrap());
let mut room_response = http::response::Room::new();
set_room_joined(&mut room_response, user_b_id);
let response = response_with_room(room_id_2, room_response);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
let room_2 = client.get_room(room_id_2).unwrap();
assert!(room_2.is_direct().await.unwrap());
}
#[async_test]
async fn test_room_encryption_state_is_and_is_not_encrypted() {
let user_id = user_id!("@raclette:patate");
let client = logged_in_base_client(Some(user_id)).await;
let room_id_0 = room_id!("!r0");
let room_id_1 = room_id!("!r1");
let room_id_2 = room_id!("!r2");
let requested_required_states = RequestedRequiredStates::from(&{
let mut request = http::Request::new();
request.room_subscriptions.insert(room_id_0.to_owned(), {
let mut room_subscription = http::request::RoomSubscription::default();
room_subscription
.required_state
.push((StateEventType::RoomEncryption, "".to_owned()));
room_subscription
});
request
});
let mut response = http::Response::new("0".to_owned());
{
let not_encrypted_room = http::response::Room::new();
let mut encrypted_room = http::response::Room::new();
set_room_is_encrypted(&mut encrypted_room, user_id);
response.rooms.insert(room_id_0.to_owned(), encrypted_room.clone());
response.rooms.insert(room_id_1.to_owned(), encrypted_room);
response.rooms.insert(room_id_2.to_owned(), not_encrypted_room);
}
client
.process_sliding_sync(
&response,
&requested_required_states,
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
client.get_room(room_id_0).unwrap().encryption_state(),
EncryptionState::Encrypted
);
assert_matches!(
client.get_room(room_id_1).unwrap().encryption_state(),
EncryptionState::Encrypted
);
assert_matches!(
client.get_room(room_id_2).unwrap().encryption_state(),
EncryptionState::NotEncrypted
)
}
#[async_test]
async fn test_room_encryption_state_is_unknown() {
let user_id = user_id!("@raclette:patate");
let client = logged_in_base_client(Some(user_id)).await;
let room_id_0 = room_id!("!r0");
let room_id_1 = room_id!("!r1");
let requested_required_states = RequestedRequiredStates::from(&http::Request::new());
let mut response = http::Response::new("0".to_owned());
{
let not_encrypted_room = http::response::Room::new();
let mut encrypted_room = http::response::Room::new();
set_room_is_encrypted(&mut encrypted_room, user_id);
response.rooms.insert(room_id_0.to_owned(), encrypted_room);
response.rooms.insert(room_id_1.to_owned(), not_encrypted_room);
}
client
.process_sliding_sync(
&response,
&requested_required_states,
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
assert_matches!(
client.get_room(room_id_0).unwrap().encryption_state(),
EncryptionState::Encrypted
);
assert_matches!(
client.get_room(room_id_1).unwrap().encryption_state(),
EncryptionState::Unknown
);
}
async fn membership(
client: &BaseClient,
room_id: &RoomId,
user_id: &UserId,
) -> MembershipState {
let room = client.get_room(room_id).expect("Room not found!");
let member = room.get_member(user_id).await.unwrap().expect("B not in room");
member.membership().clone()
}
fn direct_targets(client: &BaseClient, room_id: &RoomId) -> HashSet<OwnedDirectUserIdentifier> {
let room = client.get_room(room_id).expect("Room not found!");
room.direct_targets()
}
async fn create_dm(
client: &BaseClient,
room_id: &RoomId,
my_id: &UserId,
their_id: &UserId,
other_state: MembershipState,
) {
let mut room = http::response::Room::new();
set_room_joined(&mut room, my_id);
match other_state {
MembershipState::Join => {
room.joined_count = Some(uint!(2));
room.invited_count = None;
}
MembershipState::Invite => {
room.joined_count = Some(uint!(1));
room.invited_count = Some(uint!(1));
}
_ => {
room.joined_count = Some(uint!(1));
room.invited_count = None;
}
}
room.required_state.push(make_membership_event(their_id, other_state));
let mut response = response_with_room(room_id, room);
set_direct_with(&mut response, their_id.to_owned(), vec![room_id.to_owned()]);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
}
async fn update_room_membership(
client: &BaseClient,
room_id: &RoomId,
user_id: &UserId,
new_state: MembershipState,
) {
let mut room = http::response::Room::new();
room.required_state.push(make_membership_event(user_id, new_state));
let response = response_with_room(room_id, room);
client
.process_sliding_sync(
&response,
&RequestedRequiredStates::default(),
&client.state_store_lock().lock().await,
)
.await
.expect("Failed to process sync");
}
fn set_direct_with(
response: &mut http::Response,
user_id: OwnedUserId,
room_ids: Vec<OwnedRoomId>,
) {
let mut direct_content: BTreeMap<OwnedDirectUserIdentifier, Vec<OwnedRoomId>> =
BTreeMap::new();
direct_content.insert(user_id.into(), room_ids);
response
.extensions
.account_data
.global
.push(make_global_account_data_event(DirectEventContent(direct_content)));
}
fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
let mut response = http::Response::new("5".to_owned());
response.rooms.insert(room_id.to_owned(), room);
response
}
fn room_with_avatar(avatar_uri: &MxcUri, user_id: &UserId) -> http::response::Room {
let mut room = http::response::Room::new();
let mut avatar_event_content = RoomAvatarEventContent::new();
avatar_event_content.url = Some(avatar_uri.to_owned());
room.required_state.push(make_state_event(user_id, "", avatar_event_content, None));
room
}
fn room_with_canonical_alias(
room_alias_id: &RoomAliasId,
user_id: &UserId,
) -> http::response::Room {
let mut room = http::response::Room::new();
let mut canonical_alias_event_content = RoomCanonicalAliasEventContent::new();
canonical_alias_event_content.alias = Some(room_alias_id.to_owned());
room.required_state.push(make_state_event(
user_id,
"",
canonical_alias_event_content,
None,
));
room
}
fn room_with_name(name: &str, user_id: &UserId) -> http::response::Room {
let mut room = http::response::Room::new();
let name_event_content = RoomNameEventContent::new(name.to_owned());
room.required_state.push(make_state_event(user_id, "", name_event_content, None));
room
}
fn set_room_name(room: &mut http::response::Room, sender: &UserId, name: String) {
room.required_state.push(make_state_event(
sender,
"",
RoomNameEventContent::new(name),
None,
));
}
fn set_room_invited(room: &mut http::response::Room, inviter: &UserId, invitee: &UserId) {
let evt = Raw::new(&json!({
"type": "m.room.member",
"sender": inviter,
"content": {
"is_direct": true,
"membership": "invite",
},
"state_key": invitee,
}))
.expect("Failed to make raw event")
.cast_unchecked();
room.invite_state = Some(vec![evt]);
room.required_state.push(make_state_event(
inviter,
invitee.as_str(),
RoomMemberEventContent::new(MembershipState::Invite),
None,
));
}
fn set_room_knocked(room: &mut http::response::Room, knocker: &UserId) {
let evt = Raw::new(&json!({
"type": "m.room.member",
"sender": knocker,
"content": {
"is_direct": true,
"membership": "knock",
},
"state_key": knocker,
}))
.expect("Failed to make raw event")
.cast_unchecked();
room.invite_state = Some(vec![evt]);
}
fn set_room_joined(room: &mut http::response::Room, user_id: &UserId) {
room.required_state.push(make_membership_event(user_id, MembershipState::Join));
}
fn set_room_left(room: &mut http::response::Room, user_id: &UserId) {
room.required_state.push(make_membership_event(user_id, MembershipState::Leave));
}
fn set_room_left_as_timeline_event(room: &mut http::response::Room, user_id: &UserId) {
room.timeline.push(make_membership_event(user_id, MembershipState::Leave));
}
fn set_room_is_encrypted(room: &mut http::response::Room, user_id: &UserId) {
room.required_state.push(make_encryption_event(user_id));
}
fn make_membership_event<K>(user_id: &UserId, state: MembershipState) -> Raw<K> {
make_state_event(user_id, user_id.as_str(), RoomMemberEventContent::new(state), None)
}
fn make_encryption_event<K>(user_id: &UserId) -> Raw<K> {
make_state_event(user_id, "", RoomEncryptionEventContent::with_recommended_defaults(), None)
}
fn make_global_account_data_event<C: GlobalAccountDataEventContent, E>(content: C) -> Raw<E> {
Raw::new(&json!({
"type": content.event_type(),
"content": content,
}))
.expect("Failed to create account data event")
.cast_unchecked()
}
fn make_state_event<C: StateEventContent, E>(
sender: &UserId,
state_key: &str,
content: C,
prev_content: Option<C>,
) -> Raw<E> {
let unsigned = if let Some(prev_content) = prev_content {
json!({ "prev_content": prev_content })
} else {
json!({})
};
Raw::new(&json!({
"type": content.event_type(),
"state_key": state_key,
"content": content,
"event_id": event_id!("$evt"),
"sender": sender,
"origin_server_ts": 10,
"unsigned": unsigned,
}))
.expect("Failed to create state event")
.cast_unchecked()
}
}