From 98882b9c23bdf03f875abf79060937047425a4c3 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Thu, 6 Jul 2023 16:48:24 +0100 Subject: [PATCH 01/12] Store the last timeline event in RoomInfo and the latest few encrypted events in Room. Use the latest event to provide a room preview, and use the encrypted events to replace the laest event when they are decrypted. --- bindings/matrix-sdk-ffi/src/room_list.rs | 4 +- crates/matrix-sdk-base/src/client.rs | 173 ++++++-- crates/matrix-sdk-base/src/latest_event.rs | 201 +++++++++ crates/matrix-sdk-base/src/lib.rs | 1 + crates/matrix-sdk-base/src/rooms/normal.rs | 216 +++++++++- crates/matrix-sdk-base/src/sliding_sync.rs | 408 +++++++++++++++++- crates/matrix-sdk-crypto/src/lib.rs | 2 +- crates/matrix-sdk-crypto/src/machine.rs | 19 +- crates/matrix-sdk-crypto/src/olm/session.rs | 2 +- .../src/room_list_service/room.rs | 34 +- .../src/timeline/event_item/content.rs | 69 ++- .../src/timeline/event_item/mod.rs | 168 +++++++- .../src/timeline/sliding_sync_ext.rs | 140 +++++- .../tests/integration/room_list_service.rs | 6 +- crates/matrix-sdk/src/client/builder.rs | 37 +- crates/matrix-sdk/src/sliding_sync/client.rs | 6 +- crates/matrix-sdk/src/sliding_sync/room.rs | 7 +- 17 files changed, 1394 insertions(+), 99 deletions(-) create mode 100644 crates/matrix-sdk-base/src/latest_event.rs diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 1b59177ea..b9967f8af 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -374,9 +374,7 @@ impl RoomListItem { } fn latest_event(&self) -> Option> { - RUNTIME.block_on(async { - self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new) - }) + self.inner.latest_event().map(EventTimelineItem).map(Arc::new) } fn has_unread_notifications(&self) -> bool { diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 676c1fafc..7a9084b97 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -56,6 +56,8 @@ use tokio::sync::RwLock; use tokio::sync::RwLockReadGuard; use tracing::{debug, info, instrument, trace, warn}; +#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] +use crate::latest_event::{is_suitable_for_latest_event, PossibleLatestEvent}; use crate::{ deserialized_responses::{AmbiguityChanges, MembersResponse, SyncTimelineEvent}, error::Result, @@ -561,24 +563,84 @@ impl BaseClient { changed_devices: &api::sync::sync_events::DeviceLists, one_time_keys_counts: &BTreeMap, unused_fallback_keys: Option<&[ruma::DeviceKeyAlgorithm]>, + changes: &mut StateChanges, ) -> Result>> { if let Some(o) = self.olm_machine().await.as_ref() { // Let the crypto machine handle the sync response, this // decrypts to-device events, but leaves room events alone. // This makes sure that we have the decryption keys for the room // events at hand. - Ok(o.receive_sync_changes( - to_device_events, - changed_devices, - one_time_keys_counts, - unused_fallback_keys, - ) - .await?) + let (events, room_key_updates) = o + .receive_sync_changes( + to_device_events, + changed_devices, + one_time_keys_counts, + unused_fallback_keys, + ) + .await?; + + #[cfg(feature = "experimental-sliding-sync")] + for room_key_update in room_key_updates { + if let Some(mut room) = self.get_room(&room_key_update.room_id) { + self.decrypt_latest_events(&mut room, changes).await; + } + } + + Ok(events) } else { + // If we have no OlmMachine, just return the events that were passed in. + // This should not happen unless we forget to set things up by calling + // set_session_meta(). Ok(to_device_events) } } + /// Decrypt any of this room's latest_encrypted_events + /// that we can and if we can, change latest_event to reflect what we + /// found, and remove any older encrypted events from + /// latest_encrypted_events. + #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] + async fn decrypt_latest_events(&self, room: &mut Room, changes: &mut StateChanges) { + // Try to find a message we can decrypt and is suitable for using as the latest + // event. If we found one, set it as the latest and delete any older + // encrypted events + if let Some((found, found_index)) = self.decrypt_latest_suitable_event(room).await { + room.on_latest_event_decrypted(found, found_index); + changes.room_infos.insert(room.room_id().to_owned(), room.clone_info()); + } + } + + /// Attempt to decrypt a latest event, trying the latest stored encrypted + /// one first, and walking backwards, stopping when we find an event + /// that we can decrypt, and that is suitable to be the latest event + /// (i.e. we can usefully display it as a message preview). Returns the + /// decrypted event if we found one, along with its index in the + /// latest_encrypted_events list, or None if we didn't find one. + #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] + async fn decrypt_latest_suitable_event( + &self, + room: &Room, + ) -> Option<(SyncTimelineEvent, usize)> { + let enc_events = room.latest_encrypted_events(); + + // Walk backwards through the encrypted events, looking for one we can decrypt + for (i, event) in enc_events.iter().enumerate().rev() { + if let Ok(Some(decrypted)) = self.decrypt_sync_room_event(event, room.room_id()).await { + // We found an event we can decrypt + if let Ok(any_sync_event) = decrypted.event.deserialize() { + // We can deserialize it to find its type + if let PossibleLatestEvent::YesMessageLike(_) = + is_suitable_for_latest_event(&any_sync_event) + { + // The event is the right type for us to use as latest_event + return Some((decrypted, i)); + } + } + } + } + None + } + /// User has joined a room. /// /// Update the internal and cached state accordingly. Return the final Room. @@ -644,6 +706,7 @@ impl BaseClient { } let now = Instant::now(); + let mut changes = Box::new(StateChanges::new(response.next_batch.clone())); #[cfg(feature = "e2e-encryption")] let to_device = self @@ -652,13 +715,13 @@ impl BaseClient { &response.device_lists, &response.device_one_time_keys_count, response.device_unused_fallback_key_types.as_deref(), + &mut changes, ) .await?; #[cfg(not(feature = "e2e-encryption"))] let to_device = response.to_device.events; - let mut changes = Box::new(StateChanges::new(response.next_batch.clone())); let mut ambiguity_cache = AmbiguityCache::new(self.store.inner.clone()); self.handle_account_data(&response.account_data.events, &mut changes).await; @@ -1225,31 +1288,24 @@ impl Default for BaseClient { #[cfg(test)] mod tests { use matrix_sdk_test::{ - async_test, response_from_file, EventBuilder, InvitedRoomBuilder, LeftRoomBuilder, - StrippedStateTestEvent, TimelineTestEvent, + async_test, response_from_file, EventBuilder, InvitedRoomBuilder, JoinedRoomBuilder, + LeftRoomBuilder, StrippedStateTestEvent, TimelineTestEvent, }; use ruma::{ api::{client as api, IncomingResponse}, - room_id, user_id, + room_id, user_id, RoomId, UserId, }; use serde_json::json; use super::BaseClient; - use crate::{DisplayName, RoomState, SessionMeta}; + use crate::{DisplayName, Room, RoomState, SessionMeta, StateChanges}; #[async_test] async fn invite_after_leaving() { let user_id = user_id!("@alice:example.org"); let room_id = room_id!("!test:example.org"); - let client = BaseClient::new(); - client - .set_session_meta(SessionMeta { - user_id: user_id.to_owned(), - device_id: "FOOBAR".into(), - }) - .await - .unwrap(); + let client = logged_in_client(user_id).await; let mut ev_builder = EventBuilder::new(); @@ -1295,14 +1351,7 @@ mod tests { let user_id = user_id!("@alice:example.org"); let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org"); - let client = BaseClient::new(); - client - .set_session_meta(SessionMeta { - user_id: user_id.to_owned(), - device_id: "FOOBAR".into(), - }) - .await - .unwrap(); + let client = logged_in_client(user_id).await; let response = api::sync::sync_events::v3::Response::try_from_http_response(response_from_file(&json!({ "next_batch": "asdkl;fjasdkl;fj;asdkl;f", @@ -1384,4 +1433,72 @@ mod tests { DisplayName::Calculated("Kyra".to_owned()) ); } + + #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] + #[async_test] + async fn when_there_are_no_latest_encrypted_events_decrypting_them_does_nothing() { + // Given a room + let user_id = user_id!("@u:u.to"); + let room_id = room_id!("!r:u.to"); + let client = logged_in_client(user_id).await; + let mut room = process_room_join(&client, room_id, "$1", user_id).await; + + // Sanity: it has no latest_encrypted_events or latest_event + assert!(room.latest_encrypted_events().is_empty()); + assert!(room.latest_event().is_none()); + + // When I tell it to do some decryption + let mut changes = StateChanges::default(); + client.decrypt_latest_events(&mut room, &mut changes).await; + + // Then nothing changed + assert!(room.latest_encrypted_events().is_empty()); + assert!(room.latest_event().is_none()); + assert!(changes.room_infos.is_empty()); + } + + // TODO: I wanted to write more tests here for decrypt_latest_events but I got + // lost trying to set up my OlmMachine to be able to encrypt and decrypt + // events. In the meantime, there are tests for the most difficult logic + // inside Room. --andyb + + async fn logged_in_client(user_id: &UserId) -> BaseClient { + let client = BaseClient::new(); + client + .set_session_meta(SessionMeta { + user_id: user_id.to_owned(), + device_id: "FOOBAR".into(), + }) + .await + .expect("set_session_meta failed!"); + client + } + + #[cfg(feature = "e2e-encryption")] + async fn process_room_join( + client: &BaseClient, + room_id: &RoomId, + event_id: &str, + user_id: &UserId, + ) -> Room { + let mut ev_builder = EventBuilder::new(); + let response = ev_builder + .add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( + TimelineTestEvent::Custom(json!({ + "content": { + "displayname": "Alice", + "membership": "join", + }, + "event_id": event_id, + "origin_server_ts": 1432135524678u64, + "sender": user_id, + "state_key": user_id, + "type": "m.room.member", + })), + )) + .build_sync_response(); + client.receive_sync_response(response).await.unwrap(); + + client.get_room(room_id).expect("Just-created room not found!") + } } diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs new file mode 100644 index 000000000..3efd6b7b8 --- /dev/null +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -0,0 +1,201 @@ +//! Utilities for working with events to decide whether they are suitable for +//! use as a [RoomInfo::latest_event]. + +use ruma::events::{ + room::message::RoomMessageEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, + OriginalSyncMessageLikeEvent, SyncMessageLikeEvent, +}; + +/// Represents a decision about whether an event could be stored as the latest +/// event in a room. Variants starting with Yes indicate that this message could +/// be stored, and provide the inner event information, and those starting with +/// a No indicate that it could not, and give a reason. +#[derive(Debug)] +pub enum PossibleLatestEvent<'a> { + /// This message is suitable - it is an m.room.message + YesMessageLike(&'a OriginalSyncMessageLikeEvent), + // Later: YesState(), + // Later: YesReaction(), + /// Not suitable - it's a state event + NoUnsupportedEventType, + /// Not suitable - it's not an m.room.message + NoUnsupportedMessageLikeType, + /// Not suitable - it's encrypted + NoEncrypted, + /// Not suitable - it's redacted (might we want to include these?) + NoRedacted, +} + +/// Decide whether an event could be stored as the latest event in a room. +/// Returns a LatestEvent representing our decision. +#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] +pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> { + match event { + // Suitable - we have an m.room.message that was not redacted + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Original(message), + )) => PossibleLatestEvent::YesMessageLike(message), + + // Encrypted events are not suitable + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => { + PossibleLatestEvent::NoEncrypted + } + + // Later, if we support reactions: + // AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Reaction(_)) + + // Redacted events are not suitable + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Redacted(_), + )) => PossibleLatestEvent::NoRedacted, + + // MessageLike, but not one of the types we want to show in message previews, so not + // suitable + AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType, + + // We don't currently support state events + AnySyncTimelineEvent::State(_) => PossibleLatestEvent::NoUnsupportedEventType, + } +} + +#[cfg(test)] +mod test { + use std::collections::BTreeMap; + + use assert_matches::assert_matches; + use ruma::{ + events::{ + room::{ + encrypted::{ + EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent, + SyncRoomEncryptedEvent, + }, + message::{ + ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent, + RoomMessageEventContent, SyncRoomMessageEvent, + }, + topic::{RoomTopicEventContent, SyncRoomTopicEvent}, + ImageInfo, MediaSource, + }, + sticker::{StickerEventContent, SyncStickerEvent}, + AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey, + MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent, + RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, + UnsignedRoomRedactionEvent, + }, + owned_event_id, owned_mxc_uri, owned_user_id, MilliSecondsSinceUnixEpoch, UInt, + }; + use serde_json::json; + + use crate::latest_event::{is_suitable_for_latest_event, PossibleLatestEvent}; + + #[test] + fn room_messages_are_suitable() { + let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent { + content: RoomMessageEventContent::new(MessageType::Image( + ImageMessageEventContent::new( + "".to_owned(), + MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")), + ), + )), + event_id: owned_event_id!("$1"), + sender: owned_user_id!("@a:b.c"), + origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), + unsigned: MessageLikeUnsigned::new(), + }), + )); + let m = assert_matches::assert_matches!( + is_suitable_for_latest_event(&event), + PossibleLatestEvent::YesMessageLike(m) => m + ); + + assert_eq!(m.content.msgtype.msgtype(), "m.image"); + } + + #[test] + fn different_types_of_messagelike_are_unsuitable() { + let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker( + SyncStickerEvent::Original(OriginalSyncMessageLikeEvent { + content: StickerEventContent::new( + "sticker!".to_owned(), + ImageInfo::new(), + owned_mxc_uri!("mxc://example.com/1"), + ), + event_id: owned_event_id!("$1"), + sender: owned_user_id!("@a:b.c"), + origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), + unsigned: MessageLikeUnsigned::new(), + }), + )); + + assert_matches!( + is_suitable_for_latest_event(&event), + PossibleLatestEvent::NoUnsupportedMessageLikeType + ); + } + + #[test] + fn redacted_messages_are_unsuitable() { + // Ruma does not allow constructing UnsignedRoomRedactionEvent instances. + let room_redaction_event: UnsignedRoomRedactionEvent = serde_json::from_value(json!({ + "content": {}, + "event_id": "$redaction", + "sender": "@x:y.za", + "origin_server_ts": 223543, + "unsigned": { "reason": "foo" } + })) + .unwrap(); + + let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncRoomMessageEvent::Redacted(RedactedSyncMessageLikeEvent { + content: RedactedRoomMessageEventContent::new(), + event_id: owned_event_id!("$1"), + sender: owned_user_id!("@a:b.c"), + origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), + unsigned: RedactedUnsigned::new(room_redaction_event), + }), + )); + + assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoRedacted); + } + + #[test] + fn encrypted_messages_are_unsuitable() { + let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted( + SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent { + content: RoomEncryptedEventContent::new( + EncryptedEventScheme::OlmV1Curve25519AesSha2( + OlmV1Curve25519AesSha2Content::new(BTreeMap::new(), "".to_owned()), + ), + None, + ), + event_id: owned_event_id!("$1"), + sender: owned_user_id!("@a:b.c"), + origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), + unsigned: MessageLikeUnsigned::new(), + }), + )); + + assert_matches!(is_suitable_for_latest_event(&event), PossibleLatestEvent::NoEncrypted); + } + + #[test] + fn state_events_are_unsuitable() { + let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic( + SyncRoomTopicEvent::Original(OriginalSyncStateEvent { + content: RoomTopicEventContent::new("".to_string()), + event_id: owned_event_id!("$1"), + sender: owned_user_id!("@a:b.c"), + origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()), + unsigned: StateUnsigned::new(), + state_key: EmptyStateKey, + }), + )); + + assert_matches!( + is_suitable_for_latest_event(&event), + PossibleLatestEvent::NoUnsupportedEventType + ); + } +} diff --git a/crates/matrix-sdk-base/src/lib.rs b/crates/matrix-sdk-base/src/lib.rs index 5caa099b5..e9740f69e 100644 --- a/crates/matrix-sdk-base/src/lib.rs +++ b/crates/matrix-sdk-base/src/lib.rs @@ -27,6 +27,7 @@ mod client; pub mod debug; pub mod deserialized_responses; mod error; +pub mod latest_event; pub mod media; mod rooms; #[cfg(feature = "experimental-sliding-sync")] diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 23380e8e7..493ea8ee2 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -19,6 +19,7 @@ use std::{ use bitflags::bitflags; use futures_util::stream::{self, StreamExt}; +use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer}; use ruma::{ api::client::sync::sync_events::v3::RoomSummary as RumaSummary, events::{ @@ -36,10 +37,11 @@ use ruma::{ tombstone::RoomTombstoneEventContent, }, tag::Tags, - AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, + AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, RoomAccountDataEventType, }, room::RoomType, + serde::Raw, EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId, }; @@ -65,6 +67,18 @@ pub struct Room { own_user_id: OwnedUserId, inner: Arc>, store: Arc, + + /// The most recent few encrypted events. When the keys come through to + /// decrypt these, the most recent relevant one will replace + /// `latest_event`. (We can't tell which one is relevant until + /// they are decrypted.) + /// + /// Currently, these are held in Room rather than RoomInfo, because we were + /// not sure whether holding too many of them might make the cache too + /// slow to load on startup. Keeping them here means they are not cached + /// to disk but held in memory. + #[cfg(feature = "experimental-sliding-sync")] + pub latest_encrypted_events: Arc>>>, } /// The room summary containing member counts and members that should be used to @@ -108,6 +122,10 @@ impl From<&MembershipState> for RoomState { } impl Room { + /// The size of the latest_encrypted_events RingBuffer + #[cfg(feature = "experimental-sliding-sync")] + const MAX_ENCRYPTED_EVENTS: usize = 10; + pub(crate) fn new( own_user_id: &UserId, store: Arc, @@ -128,6 +146,10 @@ impl Room { room_id: room_info.room_id.clone(), store, inner: Arc::new(SyncRwLock::new(room_info)), + #[cfg(feature = "experimental-sliding-sync")] + latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new( + Self::MAX_ENCRYPTED_EVENTS, + ))), } } @@ -348,6 +370,41 @@ impl Room { self.calculate_name().await } + /// Return the last event in this room, if one has been cached during + /// sliding sync. + #[cfg(feature = "experimental-sliding-sync")] + pub fn latest_event(&self) -> Option { + self.inner.read().unwrap().latest_event.clone() + } + + /// Update the last event in the room + #[cfg(feature = "experimental-sliding-sync")] + pub(crate) fn set_latest_event(&self, latest_event: Option) { + self.inner.write().unwrap().latest_event = latest_event; + } + + /// Return the most recent few encrypted events. When the keys come through + /// to decrypt these, the most recent relevant one will replace + /// latest_event. (We can't tell which one is relevant until + /// they are decrypted.) + #[cfg(feature = "experimental-sliding-sync")] + pub(crate) fn latest_encrypted_events(&self) -> Vec> { + self.latest_encrypted_events.read().unwrap().iter().cloned().collect() + } + + /// Replace our latest_event with the supplied event, and delete it and all + /// older encrypted events from latest_encrypted_events, given that the + /// new event was at the supplied index in the latest_encrypted_events + /// list. + /// + /// Panics if index is not a valid index in the latest_encrypted_events + /// list. + #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] + pub(crate) fn on_latest_event_decrypted(&mut self, event: SyncTimelineEvent, index: usize) { + self.set_latest_event(Some(event)); + self.latest_encrypted_events.write().unwrap().drain(0..=index); + } + /// Get the list of users ids that are considered to be joined members of /// this room. pub async fn joined_user_ids(&self) -> StoreResult> { @@ -632,6 +689,9 @@ pub struct RoomInfo { /// Whether or not the encryption info was been synced. #[serde(default = "encryption_state_default")] // see fn docs for why we use this default encryption_state_synced: bool, + /// The last event send by sliding sync + #[cfg(feature = "experimental-sliding-sync")] + pub latest_event: Option, /// Base room info which holds some basic event contents important for the /// room state. pub(crate) base_info: BaseRoomInfo, @@ -684,6 +744,8 @@ impl RoomInfo { last_prev_batch: None, sync_info: SyncInfo::NoState, encryption_state_synced: false, + #[cfg(feature = "experimental-sliding-sync")] + latest_event: None, base_info: BaseRoomInfo::new(), } } @@ -999,6 +1061,7 @@ mod test { }; #[test] + #[cfg(feature = "experimental-sliding-sync")] fn room_info_serialization() { // This test exists to make sure we don't accidentally change the // serialized format for `RoomInfo`. @@ -1019,9 +1082,56 @@ mod test { last_prev_batch: Some("pb".to_owned()), sync_info: SyncInfo::FullySynced, encryption_state_synced: true, + latest_event: Some( + Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap().into(), + ), base_info: BaseRoomInfo::new(), }; + let info_json = json!({ + "room_id": "!gda78o:server.tld", + "room_type": "Invited", + "notification_counts": { + "highlight_count": 1, + "notification_count": 2, + }, + "summary": { + "heroes": ["Somebody"], + "joined_member_count": 5, + "invited_member_count": 0, + }, + "members_synced": true, + "last_prev_batch": "pb", + "sync_info": "FullySynced", + "encryption_state_synced": true, + "latest_event": {"encryption_info": null, "event": {"sender": "@u:i.uk"}}, + "base_info": { + "avatar": null, + "canonical_alias": null, + "create": null, + "dm_targets": [], + "encryption": null, + "guest_access": null, + "history_visibility": null, + "join_rules": null, + "max_power_level": 100, + "name": null, + "tombstone": null, + "topic": null, + } + }); + + assert_eq!(serde_json::to_value(info).unwrap(), info_json); + } + + #[test] + #[cfg(feature = "experimental-sliding-sync")] + fn room_info_deserialization_without_optional_items() { + // Ensure we can still deserialize RoomInfos before we added things to its + // schema + + // The following JSON should never change if we want to be able to read in old + // cached state let info_json = json!({ "room_id": "!gda78o:server.tld", "room_type": "Invited", @@ -1054,7 +1164,32 @@ mod test { } }); - assert_eq!(serde_json::to_value(info).unwrap(), info_json); + let info: RoomInfo = serde_json::from_value(info_json).unwrap(); + + assert_eq!(info.room_id, room_id!("!gda78o:server.tld")); + assert_eq!(info.room_state, RoomState::Invited); + assert_eq!(info.notification_counts.highlight_count, 1); + assert_eq!(info.notification_counts.notification_count, 2); + assert_eq!(info.summary.heroes, vec!["Somebody".to_owned()]); + assert_eq!(info.summary.joined_member_count, 5); + assert_eq!(info.summary.invited_member_count, 0); + assert!(info.members_synced); + assert_eq!(info.last_prev_batch, Some("pb".to_owned())); + assert_eq!(info.sync_info, SyncInfo::FullySynced); + assert!(info.encryption_state_synced); + assert!(info.latest_event.is_none()); + assert!(info.base_info.avatar.is_none()); + assert!(info.base_info.canonical_alias.is_none()); + assert!(info.base_info.create.is_none()); + assert_eq!(info.base_info.dm_targets.len(), 0); + assert!(info.base_info.encryption.is_none()); + assert!(info.base_info.guest_access.is_none()); + assert!(info.base_info.history_visibility.is_none()); + assert!(info.base_info.join_rules.is_none()); + assert_eq!(info.base_info.max_power_level, 100); + assert!(info.base_info.name.is_none()); + assert!(info.base_info.tombstone.is_none()); + assert!(info.base_info.topic.is_none()); } fn make_room(room_type: RoomState) -> (Arc, Room) { @@ -1312,4 +1447,81 @@ mod test { "new name" ); } + + #[test] + #[cfg(feature = "experimental-sliding-sync")] + fn when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() { + // Given a room with an encrypted event + let (_store, mut room) = make_room(RoomState::Joined); + add_encrypted_event(&mut room, "$A"); + // Sanity: it has no latest_event + assert!(room.latest_event().is_none()); + + // When I provide a decrypted event to replace the encrypted one + let event = make_event("$A"); + room.on_latest_event_decrypted(event.clone(), 0); + + // Then is it stored + assert_eq!(room.latest_event().unwrap().event_id(), event.event_id()); + } + + #[test] + #[cfg(feature = "experimental-sliding-sync")] + fn when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() { + // Given a room with some encrypted events and a latest event + let (_store, mut room) = make_room(RoomState::Joined); + room.inner.write().unwrap().latest_event = Some(make_event("$A")); + add_encrypted_event(&mut room, "$0"); + add_encrypted_event(&mut room, "$1"); + add_encrypted_event(&mut room, "$2"); + add_encrypted_event(&mut room, "$3"); + + // When I provide a latest event + let new_event = make_event("$1"); + let new_event_index = 1; + room.on_latest_event_decrypted(new_event.clone(), new_event_index); + + // Then the encrypted events list is shortened to only newer events + let enc_evs = room.latest_encrypted_events(); + assert_eq!(enc_evs.len(), 2); + assert_eq!(enc_evs.get(0).unwrap().get_field::<&str>("event_id").unwrap().unwrap(), "$2"); + assert_eq!(enc_evs.get(1).unwrap().get_field::<&str>("event_id").unwrap().unwrap(), "$3"); + + // And the event is stored + assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id()); + } + + #[test] + #[cfg(feature = "experimental-sliding-sync")] + fn replacing_the_newest_event_leaves_none_left() { + // Given a room with some encrypted events + let (_store, mut room) = make_room(RoomState::Joined); + add_encrypted_event(&mut room, "$0"); + add_encrypted_event(&mut room, "$1"); + add_encrypted_event(&mut room, "$2"); + add_encrypted_event(&mut room, "$3"); + + // When I provide a latest event and say it was the very latest + let new_event = make_event("$3"); + let new_event_index = 3; + room.on_latest_event_decrypted(new_event.clone(), new_event_index); + + // Then the encrypted events list ie empty + let enc_evs = room.latest_encrypted_events(); + assert_eq!(enc_evs.len(), 0); + } + + #[cfg(feature = "experimental-sliding-sync")] + fn add_encrypted_event(room: &mut Room, event_id: &str) { + room.latest_encrypted_events + .write() + .unwrap() + .push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap()); + } + + fn make_event(event_id: &str) -> SyncTimelineEvent { + SyncTimelineEvent::new( + Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(), + ) + } } diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 83e4df0f7..da86dbb7c 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -15,6 +15,7 @@ #[cfg(feature = "e2e-encryption")] use std::ops::Deref; +use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; use ruma::{ api::client::sync::sync_events::{ v3::{self, InvitedRoom, RoomSummary}, @@ -23,7 +24,7 @@ use ruma::{ events::AnySyncStateEvent, RoomId, }; -use tracing::{debug, info, instrument}; +use tracing::{debug, info, instrument, warn}; use super::BaseClient; #[cfg(feature = "e2e-encryption")] @@ -31,6 +32,7 @@ use crate::RoomMemberships; use crate::{ deserialized_responses::AmbiguityChanges, error::Result, + latest_event::{is_suitable_for_latest_event, PossibleLatestEvent}, rooms::RoomState, store::{ambiguity_map::AmbiguityCache, StateChanges, Store}, sync::{JoinedRoom, Rooms, SyncResponse}, @@ -78,6 +80,8 @@ impl BaseClient { e2ee.device_unused_fallback_key_types.as_ref().map(|v| v.len()) ); + let mut changes = StateChanges::default(); + // Process the to-device events and other related e2ee data. This returns a list // of all the to-device events that were passed in but encrypted ones // were replaced with their decrypted version. @@ -91,11 +95,11 @@ impl BaseClient { &e2ee.device_lists, &e2ee.device_one_time_keys_count, e2ee.device_unused_fallback_key_types.as_deref(), + &mut changes, ) .await?; let store = self.store.clone(); - let mut changes = StateChanges::default(); let mut ambiguity_cache = AmbiguityCache::new(store.inner.clone()); if !account_data.is_empty() { @@ -195,7 +199,7 @@ impl BaseClient { let required_state = Self::deserialize_events(&room_data.required_state); // Find or create the room in the store - let (room, mut room_info, invited_room) = self + let (mut room, mut room_info, invited_room) = self .process_sliding_sync_room_membership( room_data, &required_state, @@ -245,6 +249,10 @@ impl BaseClient { ) .await?; + // Cache the latest decrypted event in room_info, and also keep any later + // encrypted events, so we can slot them in when we get the keys. + cache_latest_events(&mut room, &mut room_info, &timeline.events); + #[cfg(feature = "e2e-encryption")] if room_info.is_encrypted() { if let Some(o) = self.olm_machine().await.as_ref() { @@ -363,6 +371,56 @@ impl BaseClient { } } +/// Find the most recent decrypted event and cache it in the supplied RoomInfo. +/// If any encrypted events are found after that one, store them in the RoomInfo +/// too so we can use them when we get the relevant keys. +fn cache_latest_events(room: &mut Room, room_info: &mut RoomInfo, events: &[SyncTimelineEvent]) { + let mut encrypted_events = + Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity()); + for e in events.iter().rev() { + if let Ok(timeline_event) = e.event.deserialize() { + match is_suitable_for_latest_event(&timeline_event) { + PossibleLatestEvent::YesMessageLike(_) => { + // m.room.message - we found one! Store it. + + // Store it in the return RoomInfo, and in the Room, to make sure they are + // consistent + room_info.latest_event = Some(e.clone()); + room.set_latest_event(Some(e.clone())); + // We don't need any of the older encrypted events because we have a new + // decrypted one. + room.latest_encrypted_events.write().unwrap().clear(); + // We can stop looking through the timeline now because everything else is + // older. + break; + } + PossibleLatestEvent::NoEncrypted => { + // m.room.encrypted - this might be the latest event later - we can't tell until + // we are able to decrypt it, so store it for now + // + // Check how many encrypted events we have seen. Only store another if we + // haven't already stored the maximum number. + if encrypted_events.len() < encrypted_events.capacity() { + encrypted_events.push(e.event.clone()); + } + } + _ => { + // Ignore unsuitable events + } + } + } else { + warn!( + "Failed to deserialise event as AnySyncTimelineEvent. ID={}", + e.event_id().expect("Event has no ID!") + ); + } + } + + // Push the encrypted events we found into the Room, in reverse order, so + // the latest is last + room.latest_encrypted_events.write().unwrap().extend(encrypted_events.into_iter().rev()); +} + fn process_room_properties(room_data: &v4::SlidingSyncRoom, room_info: &mut RoomInfo) { if let Some(name) = &room_data.name { room_info.update_name(name.to_owned()); @@ -387,8 +445,12 @@ fn process_room_properties(room_data: &v4::SlidingSyncRoom, room_info: &mut Room #[cfg(test)] mod test { - use std::collections::{BTreeMap, HashSet}; + use std::{ + collections::{BTreeMap, HashSet}, + sync::{Arc, RwLock as SyncRwLock}, + }; + use matrix_sdk_common::ring_buffer::RingBuffer; use matrix_sdk_test::async_test; use ruma::{ device_id, event_id, @@ -399,7 +461,8 @@ mod test { canonical_alias::RoomCanonicalAliasEventContent, member::{MembershipState, RoomMemberEventContent}, }, - AnySyncStateEvent, GlobalAccountDataEventContent, StateEventContent, + AnySyncStateEvent, AnySyncTimelineEvent, GlobalAccountDataEventContent, + StateEventContent, }, mxc_uri, room_alias_id, room_id, serde::Raw, @@ -408,7 +471,7 @@ mod test { use serde_json::json; use super::*; - use crate::SessionMeta; + use crate::{store::MemoryStore, SessionMeta}; #[async_test] async fn can_process_empty_sliding_sync_response() { @@ -669,6 +732,328 @@ mod test { ); } + #[async_test] + async fn last_event_from_sliding_sync_is_cached() { + // Given a logged-in client + let client = logged_in_client().await; + let room_id = room_id!("!r:e.uk"); + let event_a = json!({ + "sender":"@alice:example.com", + "type":"m.room.message", + "event_id": "$ida", + "origin_server_ts": 12344446, + "content":{"body":"A", "msgtype": "m.text"} + }); + let event_b = json!({ + "sender":"@alice:example.com", + "type":"m.room.message", + "event_id": "$idb", + "origin_server_ts": 12344447, + "content":{"body":"B", "msgtype": "m.text"} + }); + + // When the sliding sync response contains a timeline + let events = &[event_a, event_b.clone()]; + let room = room_with_timeline(events); + let response = response_with_room(room_id, room).await; + client.process_sliding_sync(&response).await.expect("Failed to process sync"); + + // Then the room holds the latest event + let client_room = client.get_room(room_id).expect("No room found"); + assert_eq!(ev_id(client_room.latest_event()), "$idb"); + } + + #[test] + fn when_no_events_we_dont_cache_any() { + let events = &[]; + let chosen = choose_event_to_cache(events); + assert!(chosen.is_none()); + } + + #[test] + fn when_only_one_event_we_cache_it() { + let event1 = make_event("m.room.message", "$1"); + let events = &[event1.clone()]; + let chosen = choose_event_to_cache(events); + assert_eq!(ev_id(chosen), rawev_id(event1)); + } + + #[test] + fn with_multiple_events_we_cache_the_last_one() { + let event1 = make_event("m.room.message", "$1"); + let event2 = make_event("m.room.message", "$2"); + let events = &[event1, event2.clone()]; + let chosen = choose_event_to_cache(events); + assert_eq!(ev_id(chosen), rawev_id(event2)); + } + + #[test] + fn cache_the_latest_relevant_event_and_ignore_irrelevant_ones_even_if_later() { + let event1 = make_event("m.room.message", "$1"); + let event2 = make_event("m.room.message", "$2"); + let event3 = make_event("m.room.powerlevels", "$3"); + let event4 = make_event("m.room.powerlevels", "$5"); + let events = &[event1, event2.clone(), event3, event4]; + let chosen = choose_event_to_cache(events); + assert_eq!(ev_id(chosen), rawev_id(event2)); + } + + #[test] + fn prefer_to_cache_nothing_rather_than_irrelevant_events() { + let event1 = make_event("m.room.power_levels", "$1"); + let events = &[event1]; + let chosen = choose_event_to_cache(events); + assert!(chosen.is_none()); + } + + #[test] + fn cache_encrypted_events_that_are_after_latest_message() { + // Given two message events followed by two encrypted + let event1 = make_event("m.room.message", "$1"); + let event2 = make_event("m.room.message", "$2"); + let event3 = make_encrypted_event("$3"); + let event4 = make_encrypted_event("$4"); + let events = &[event1.clone(), event2.clone(), event3.clone(), event4.clone()]; + + // When I ask to cache events + let mut room = make_room(); + let mut room_info = room.clone_info(); + cache_latest_events(&mut room, &mut room_info, events); + + // The latest message is stored + assert_eq!(ev_id(room_info.latest_event), rawev_id(event2.clone())); + assert_eq!(ev_id(room.latest_event()), rawev_id(event2)); + + // And also the two encrypted ones + assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3, event4])); + } + + #[test] + fn dont_cache_encrypted_events_that_are_before_latest_message() { + // Given an encrypted event before and after the message + let event1 = make_encrypted_event("$1"); + let event2 = make_event("m.room.message", "$2"); + let event3 = make_encrypted_event("$3"); + let events = &[event1.clone(), event2.clone(), event3.clone()]; + + // When I ask to cache events + let mut room = make_room(); + let mut room_info = room.clone_info(); + cache_latest_events(&mut room, &mut room_info, events); + + // The latest message is stored + assert_eq!(ev_id(room.latest_event()), rawev_id(event2)); + + // And also the encrypted one that was after it, but not the one before + assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3])); + } + + #[test] + fn skip_irrelevant_events_eg_receipts_even_if_after_message() { + // Given two message events followed by two encrypted, with a receipt in the + // middle + let event1 = make_event("m.room.message", "$1"); + let event2 = make_event("m.room.message", "$2"); + let event3 = make_encrypted_event("$3"); + let event4 = make_event("m.read", "$4"); + let event5 = make_encrypted_event("$5"); + let events = &[event1.clone(), event2.clone(), event3.clone(), event4, event5.clone()]; + + // When I ask to cache events + let mut room = make_room(); + let mut room_info = room.clone_info(); + cache_latest_events(&mut room, &mut room_info, events); + + // The latest message is stored, ignoring the receipt + assert_eq!(ev_id(room.latest_event()), rawev_id(event2)); + + // The two encrypted ones are stored, but not the receipt + assert_eq!(rawevs_ids(&room.latest_encrypted_events), evs_ids(&[event3, event5])); + } + + #[test] + fn only_store_the_max_number_of_encrypted_events() { + // Given two message events followed by lots of encrypted and other irrelevant + // events + let evente = make_event("m.room.message", "$e"); + let eventd = make_event("m.room.message", "$d"); + let eventc = make_encrypted_event("$c"); + let event9 = make_encrypted_event("$9"); + let event8 = make_encrypted_event("$8"); + let event7 = make_encrypted_event("$7"); + let eventb = make_event("m.read", "$b"); + let event6 = make_encrypted_event("$6"); + let event5 = make_encrypted_event("$5"); + let event4 = make_encrypted_event("$4"); + let event3 = make_encrypted_event("$3"); + let event2 = make_encrypted_event("$2"); + let eventa = make_event("m.read", "$a"); + let event1 = make_encrypted_event("$1"); + let event0 = make_encrypted_event("$0"); + let events = &[ + evente.clone(), + eventd.clone(), + eventc.clone(), + event9.clone(), + event8.clone(), + event7.clone(), + eventb.clone(), + event6.clone(), + event5.clone(), + event4.clone(), + event3.clone(), + event2.clone(), + eventa.clone(), + event1.clone(), + event0.clone(), + ]; + + // When I ask to cache events + let mut room = make_room(); + let mut room_info = room.clone_info(); + cache_latest_events(&mut room, &mut room_info, events); + + // The latest message is stored, ignoring encrypted and receipts + assert_eq!(ev_id(room.latest_event()), rawev_id(eventd)); + + // Only 10 encrypted are stored, even though there were more + assert_eq!( + rawevs_ids(&room.latest_encrypted_events), + evs_ids(&[ + event9, event8, event7, event6, event5, event4, event3, event2, event1, event0 + ]) + ); + } + + #[test] + fn dont_overflow_capacity_if_previous_encrypted_events_exist() { + // Given a RoomInfo with lots of encrypted events already inside it + let mut room = make_room(); + let mut room_info = room.clone_info(); + cache_latest_events( + &mut room, + &mut room_info, + &[ + make_encrypted_event("$0"), + make_encrypted_event("$1"), + make_encrypted_event("$2"), + make_encrypted_event("$3"), + make_encrypted_event("$4"), + make_encrypted_event("$5"), + make_encrypted_event("$6"), + make_encrypted_event("$7"), + make_encrypted_event("$8"), + make_encrypted_event("$9"), + ], + ); + // Sanity: room_info has 10 encrypted events inside it + assert_eq!(room.latest_encrypted_events.read().unwrap().len(), 10); + + // When I ask to cache more encrypted events + let eventa = make_encrypted_event("$a"); + let mut room_info = room.clone_info(); + cache_latest_events(&mut room, &mut room_info, &[eventa]); + + // The oldest event is gone + assert!(!rawevs_ids(&room.latest_encrypted_events).contains(&"$0".to_owned())); + + // The newest event is last in the list + assert_eq!(rawevs_ids(&room.latest_encrypted_events)[9], "$a"); + } + + #[test] + fn existing_encrypted_events_are_deleted_if_we_receive_unencrypted() { + // Given a RoomInfo with some encrypted events already inside it + let mut room = make_room(); + let mut room_info = room.clone_info(); + cache_latest_events( + &mut room, + &mut room_info, + &[make_encrypted_event("$0"), make_encrypted_event("$1"), make_encrypted_event("$2")], + ); + + // When I ask to cache an unecnrypted event, and some more encrypted events + let eventa = make_event("m.room.message", "$a"); + let eventb = make_encrypted_event("$b"); + cache_latest_events(&mut room, &mut room_info, &[eventa, eventb]); + + // The only encrypted events stored are the ones after the decrypted one + assert_eq!(rawevs_ids(&room.latest_encrypted_events), &["$b"]); + + // The decrypted one is stored as the latest + assert_eq!(rawev_id(room.latest_event().unwrap()), "$a"); + } + + fn choose_event_to_cache(events: &[SyncTimelineEvent]) -> Option { + let mut room = make_room(); + let mut room_info = room.clone_info(); + cache_latest_events(&mut room, &mut room_info, events); + room.latest_event() + } + + fn rawev_id(event: SyncTimelineEvent) -> String { + event.event_id().unwrap().to_string() + } + + fn ev_id(event: Option) -> String { + event.unwrap().event_id().unwrap().to_string() + } + + fn rawevs_ids(events: &Arc>>>) -> Vec { + events.read().unwrap().iter().map(|e| e.get_field("event_id").unwrap().unwrap()).collect() + } + + fn evs_ids(events: &[SyncTimelineEvent]) -> Vec { + events.iter().map(|e| e.event_id().unwrap().to_string()).collect() + } + + fn make_room() -> Room { + Room::new( + user_id!("@u:e.co"), + Arc::new(MemoryStore::new()), + room_id!("!r:e.co"), + RoomState::Joined, + ) + } + + fn make_event(typ: &str, id: &str) -> SyncTimelineEvent { + SyncTimelineEvent::new( + Raw::from_json_string( + json!({ + "type": typ, + "event_id": id, + "content": { "msgtype": "m.text", "body": "my msg" }, + "sender": "@u:h.uk", + "origin_server_ts": 12344445, + }) + .to_string(), + ) + .unwrap(), + ) + } + + fn make_encrypted_event(id: &str) -> SyncTimelineEvent { + SyncTimelineEvent::new( + Raw::from_json_string( + json!({ + "type": "m.room.encrypted", + "event_id": id, + "content": { + "algorithm": "m.megolm.v1.aes-sha2", + "ciphertext": "", + "sender_key": "", + "device_id": "", + "session_id": "", + }, + "sender": "@u:h.uk", + "origin_server_ts": 12344445, + }) + .to_string(), + ) + .unwrap(), + ) + } + async fn membership( client: &BaseClient, room_id: &RoomId, @@ -776,6 +1161,17 @@ mod test { room } + fn room_with_timeline(events: &[serde_json::Value]) -> v4::SlidingSyncRoom { + let mut room = v4::SlidingSyncRoom::new(); + room.timeline.extend( + events + .iter() + .map(|e| Raw::from_json_string(e.to_string()).unwrap()) + .collect::>(), + ); + room + } + fn set_room_invited(room: &mut v4::SlidingSyncRoom, user_id: &UserId) { // MSC3575 shows an almost-empty event to indicate that we are invited to a // room. Just the type is supplied. diff --git a/crates/matrix-sdk-crypto/src/lib.rs b/crates/matrix-sdk-crypto/src/lib.rs index fb58a2dcd..dfa1e88c5 100644 --- a/crates/matrix-sdk-crypto/src/lib.rs +++ b/crates/matrix-sdk-crypto/src/lib.rs @@ -81,7 +81,7 @@ pub use identities::{ pub use machine::OlmMachine; #[cfg(feature = "qrcode")] pub use matrix_sdk_qrcode; -pub use olm::{CrossSigningStatus, EncryptionSettings, ReadOnlyAccount}; +pub use olm::{CrossSigningStatus, EncryptionSettings, ReadOnlyAccount, Session}; pub use requests::{ IncomingResponse, KeysBackupRequest, KeysQueryRequest, OutgoingRequest, OutgoingRequests, OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest, diff --git a/crates/matrix-sdk-crypto/src/machine.rs b/crates/matrix-sdk-crypto/src/machine.rs index e1a1c071f..e0cc704f5 100644 --- a/crates/matrix-sdk-crypto/src/machine.rs +++ b/crates/matrix-sdk-crypto/src/machine.rs @@ -68,7 +68,7 @@ use crate::{ session_manager::{GroupSessionManager, SessionManager}, store::{ locks::LockStoreError, Changes, DeviceChanges, DynCryptoStore, IdentityChanges, - IntoCryptoStore, MemoryStore, Result as StoreResult, SecretImportError, Store, + IntoCryptoStore, MemoryStore, Result as StoreResult, RoomKeyInfo, SecretImportError, Store, }, types::{ events::{ @@ -1104,6 +1104,8 @@ impl OlmMachine { /// response returned. /// /// [`decrypt_room_event`]: #method.decrypt_room_event + /// + /// Returns a tuple of (pending verification events, updated room keys) #[instrument(skip_all)] pub async fn receive_sync_changes( &self, @@ -1111,7 +1113,7 @@ impl OlmMachine { changed_devices: &DeviceLists, one_time_keys_counts: &BTreeMap, unused_fallback_keys: Option<&[DeviceKeyAlgorithm]>, - ) -> OlmResult>> { + ) -> OlmResult<(Vec>, Vec)> { // Remove verification objects that have expired or are done. let mut events = self.inner.verification_machine.garbage_collect(); @@ -1136,6 +1138,11 @@ impl OlmMachine { events.push(raw_event); } + // Technically save_changes also does the same work, so if it's slow we could + // refactor this to do it only once. + let room_key_updates: Vec<_> = + changes.inbound_group_sessions.iter().map(RoomKeyInfo::from).collect(); + let changed_sessions = self.inner.key_request_machine.collect_incoming_key_requests().await?; @@ -1143,7 +1150,7 @@ impl OlmMachine { self.store().save_changes(changes).await?; - Ok(events) + Ok((events, room_key_updates)) } /// Request a room key from our devices. @@ -2419,7 +2426,7 @@ pub(crate) mod tests { let alice_session = alice.inner.group_session_manager.get_outbound_group_session(room_id).unwrap(); - let decrypted = bob + let (decrypted, room_key_updates) = bob .receive_sync_changes(vec![event], &Default::default(), &Default::default(), None) .await .unwrap(); @@ -2437,6 +2444,10 @@ pub(crate) mod tests { bob.store().get_inbound_group_session(room_id, alice_session.session_id()).await; assert!(session.unwrap().is_some()); + + assert_eq!(room_key_updates.len(), 1); + assert_eq!(room_key_updates[0].room_id, room_id); + assert_eq!(room_key_updates[0].session_id, alice_session.session_id()); } #[async_test] diff --git a/crates/matrix-sdk-crypto/src/olm/session.rs b/crates/matrix-sdk-crypto/src/olm/session.rs index 5663d7126..077463da1 100644 --- a/crates/matrix-sdk-crypto/src/olm/session.rs +++ b/crates/matrix-sdk-crypto/src/olm/session.rs @@ -73,7 +73,7 @@ impl fmt::Debug for Session { impl Session { /// Decrypt the given Olm message. /// - /// Returns the decrypted plaintext or an `DecrypitonError` if decryption + /// Returns the decrypted plaintext or an `DecryptionError` if decryption /// failed. /// /// # Arguments diff --git a/crates/matrix-sdk-ui/src/room_list_service/room.rs b/crates/matrix-sdk-ui/src/room_list_service/room.rs index 1a9e6e71b..052061049 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/room.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/room.rs @@ -24,7 +24,10 @@ use ruma::{ }; use super::Error; -use crate::{timeline::EventTimelineItem, Timeline}; +use crate::{ + timeline::{EventTimelineItem, SlidingSyncRoomExt}, + Timeline, +}; /// A room in the room list. /// @@ -47,10 +50,6 @@ struct RoomInner { /// The timeline of the room. timeline: AsyncOnceCell>, - - /// The “sneaky” timeline of the room, i.e. this timeline doesn't track the - /// read marker nor the receipts. - sneaky_timeline: AsyncOnceCell>, } impl Room { @@ -70,7 +69,6 @@ impl Room { sliding_sync_room, room, timeline: AsyncOnceCell::new(), - sneaky_timeline: AsyncOnceCell::new(), }), }) } @@ -131,28 +129,14 @@ impl Room { .clone() } - /// Get the latest event of the timeline. + /// Get the latest event in the timeline, if we already have it cached. Does + /// not fetch any events or calculate anything - if it's not already + /// available, we return None. /// /// It's different from `Self::timeline().latest_event()` as it won't track /// the read marker and receipts. - pub async fn latest_event(&self) -> Option { - self.inner - .sneaky_timeline - .get_or_init(async { - Arc::new( - Timeline::builder(&self.inner.room) - .events( - self.inner.sliding_sync_room.prev_batch(), - self.inner.sliding_sync_room.timeline_queue(), - ) - .read_only() - .build() - .await, - ) - }) - .await - .latest_event() - .await + pub fn latest_event(&self) -> Option { + self.inner.sliding_sync_room.latest_timeline_item() } /// Is there any unread notifications? diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content.rs index e0569ffff..8538be03a 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content.rs @@ -18,6 +18,7 @@ use imbl::{vector, Vector}; use indexmap::IndexMap; use itertools::Itertools; use matrix_sdk::{deserialized_responses::TimelineEvent, Result}; +use matrix_sdk_base::latest_event::{is_suitable_for_latest_event, PossibleLatestEvent}; use ruma::{ assign, events::{ @@ -52,12 +53,12 @@ use ruma::{ space::{child::SpaceChildEventContent, parent::SpaceParentEventContent}, sticker::StickerEventContent, AnyFullStateEventContent, AnyMessageLikeEventContent, AnySyncMessageLikeEvent, - AnyTimelineEvent, BundledMessageLikeRelations, FullStateEventContent, MessageLikeEventType, - StateEventType, + AnySyncTimelineEvent, AnyTimelineEvent, BundledMessageLikeRelations, FullStateEventContent, + MessageLikeEventType, OriginalSyncMessageLikeEvent, StateEventType, }, OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedTransactionId, OwnedUserId, UserId, }; -use tracing::{debug, error}; +use tracing::{debug, error, warn}; use super::{EventTimelineItem, Profile, TimelineDetails}; use crate::timeline::{ @@ -111,6 +112,68 @@ pub enum TimelineItemContent { } impl TimelineItemContent { + /// If the supplied event is suitable to be used as a latest_event in a + /// message preview, extract its contents and wrap it as a + /// TimelineItemContent. + #[cfg(feature = "experimental-sliding-sync")] + pub(crate) fn from_latest_event_content( + event: AnySyncTimelineEvent, + ) -> Option { + match is_suitable_for_latest_event(&event) { + PossibleLatestEvent::YesMessageLike(m) => Self::from_suitable_latest_event_content(m), + PossibleLatestEvent::NoUnsupportedEventType => { + // TODO: when we support state events in message previews, this will need change + warn!("Found a state event cached as latest_event! ID={}", event.event_id()); + None + } + PossibleLatestEvent::NoUnsupportedMessageLikeType => { + // TODO: When we support reactions in message previews, this will need to change + warn!( + "Found an event cached as latest_event, but I don't know how \ + to wrap it in a TimelineItemContent. type={}, ID={}", + event.event_type().to_string(), + event.event_id() + ); + None + } + PossibleLatestEvent::NoEncrypted => { + warn!("Found an encrypted event cached as latest_event! ID={}", event.event_id()); + None + } + PossibleLatestEvent::NoRedacted => { + warn!("Found a redacted event cached as latest_event! ID={}", event.event_id()); + None + } + } + } + + /// Given some message content that is from an event that we have already + /// determined is suitable for use as a latest event in a message preview, + /// extract its contents and wrap it as a TimelineItemContent. + fn from_suitable_latest_event_content( + message: &OriginalSyncMessageLikeEvent, + ) -> Option { + // Grab the content of this event + let event_content = message.content.clone(); + + // We don't have access to any relations via the AnySyncTimelineEvent (I think - + // andyb) so we pretend there are none. This might be OK for the message preview + // use case. + let relations = BundledMessageLikeRelations::new(); + + // If this message is a reply, we would look up in this list the message it was + // replying to. Since we probably won't show this in the message preview, + // it's probably OK to supply an empty list here. + // Message::from_event marks the original event as Unavailable if it can't be + // found inside the timeline_items. + let timeline_items = Vector::new(); + Some(TimelineItemContent::Message(Message::from_event( + event_content, + relations, + &timeline_items, + ))) + } + /// If `self` is of the [`Message`][Self::Message] variant, return the inner /// [`Message`]. pub fn as_message(&self) -> Option<&Message> { diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index 1030cf0d1..cead2d070 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -15,13 +15,15 @@ use std::sync::Arc; use indexmap::IndexMap; -use matrix_sdk::{deserialized_responses::EncryptionInfo, Error}; +use matrix_sdk::{deserialized_responses::EncryptionInfo, Error, SlidingSyncRoom}; +use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; use once_cell::sync::Lazy; use ruma::{ events::{receipt::Receipt, room::message::MessageType, AnySyncTimelineEvent}, serde::Raw, EventId, MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedUserId, TransactionId, UserId, }; +use tracing::warn; mod content; mod local; @@ -78,6 +80,69 @@ impl EventTimelineItem { Self { sender, sender_profile, timestamp, content, kind } } + /// If the supplied low-level SyncTimelineEventy is suitable for use as the + /// latest_event in a message preview, wrap it as an EventTimelineItem, + #[cfg(feature = "experimental-sliding-sync")] + pub(crate) fn from_latest_event( + room: &SlidingSyncRoom, + sync_event: SyncTimelineEvent, + ) -> Option { + let raw_sync_event = sync_event.event; + + let encryption_info = sync_event.encryption_info; + + let Ok(event) = raw_sync_event.deserialize_as::() else { + warn!("Unable to deserialize latest_event as an AnySyncTimelineEvent!"); + return None; + }; + + let timestamp = event.origin_server_ts(); + let sender = event.sender().to_owned(); + let event_id = event.event_id().to_owned(); + let is_own = room.client().user_id().map(|uid| uid == sender).unwrap_or(false); + + // If we don't (yet) know how to handle this type of message, return None here. + // If we do, convert it into a TimelineItemContent. + let item_content = TimelineItemContent::from_latest_event_content(event)?; + + // We don't currently bundle any reactions with the main event. This could + // conceivably be wanted in the message preview in future. + let reactions = IndexMap::new(); + + // The message preview probably never needs read receipts. + let read_receipts = IndexMap::new(); + + // Being highlighted is _probably_ not relevant to the message preview. + let is_highlighted = false; + + // We may need this, depending on how we are going to display edited messages in + // previews. + let latest_edit_json = None; + + // Probably the origin of the event doesn't matter for the preview. + let origin = RemoteEventOrigin::Sync; + + let event_kind = RemoteEventTimelineItem { + event_id, + reactions, + read_receipts, + is_own, + is_highlighted, + encryption_info, + original_json: raw_sync_event, + latest_edit_json, + origin, + } + .into(); + + // If we need to sender profiles in the message previews, we will need to + // cache the contents of a Profile struct inside RoomInfo similar to how we + // are caching the event at the moment. + let sender_profile = TimelineDetails::Unavailable; + + Some(EventTimelineItem::new(sender, sender_profile, timestamp, item_content, event_kind)) + } + /// Check whether this item is a local echo. /// /// This returns `true` for events created locally, until the server echoes @@ -382,3 +447,104 @@ pub enum EventItemOrigin { /// The event came from pagination. Pagination, } + +#[cfg(test)] +mod test { + use assert_matches::assert_matches; + use matrix_sdk::{config::RequestConfig, Client, ClientBuilder}; + use matrix_sdk_base::{BaseClient, SessionMeta}; + use matrix_sdk_test::async_test; + use ruma::{ + api::{client::sync::sync_events::v4, MatrixVersion}, + device_id, + events::room::message::MessageFormat, + room_id, user_id, RoomId, UInt, + }; + use serde_json::json; + + use super::*; + + #[async_test] + #[cfg(feature = "experimental-sliding-sync")] + async fn latest_message_event_can_be_wrapped_as_a_timeline_item() { + // Given a sync event that is suitable to be used as a latest_event + + let room_id = room_id!("!q:x.uk"); + let user_id = user_id!("@t:o.uk"); + let event = message_event(room_id, user_id, "**My M**", "My M", 122344); + let room = SlidingSyncRoom::new( + logged_in_client(None).await, + room_id.to_owned(), + v4::SlidingSyncRoom::new(), + Vec::new(), + ); + + // When we construct a timeline event from it + let timeline_item = EventTimelineItem::from_latest_event(&room, event).unwrap(); + + // Then its properieis correctly translate + assert_eq!(timeline_item.sender, user_id); + assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable); + assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap()); + if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() { + assert_eq!(txt.body, "**My M**"); + let formatted = txt.formatted.as_ref().unwrap(); + assert_eq!(formatted.format, MessageFormat::Html); + assert_eq!(formatted.body, "My M"); + } else { + panic!("Unexpected message type"); + } + } + + fn message_event( + room_id: &RoomId, + user_id: &UserId, + body: &str, + formatted_body: &str, + ts: u64, + ) -> SyncTimelineEvent { + SyncTimelineEvent::new( + Raw::from_json_string( + json!({ + "event_id": "$eventid6", + "sender": user_id, + "origin_server_ts": ts, + "type": "m.room.message", + "room_id": room_id.to_string(), + "content": { + "body": body, + "format": "org.matrix.custom.html", + "formatted_body": formatted_body, + "msgtype": "m.text" + }, + }) + .to_string(), + ) + .unwrap(), + ) + } + + /// Copied from matrix_sdk_base::sliding_sync::test + async fn logged_in_client(homeserver_url: Option) -> Client { + let base_client = BaseClient::new(); + base_client + .set_session_meta(SessionMeta { + user_id: user_id!("@u:e.uk").to_owned(), + device_id: device_id!("XYZ").to_owned(), + }) + .await + .expect("Failed to set session meta"); + + test_client_builder(homeserver_url) + .request_config(RequestConfig::new().disable_retry()) + .base_client(base_client) + .build() + .await + .unwrap() + } + + fn test_client_builder(homeserver_url: Option) -> ClientBuilder { + let homeserver = homeserver_url.as_deref().unwrap_or("http://localhost:1234"); + Client::builder().homeserver_url(homeserver).server_versions([MatrixVersion::V1_0]) + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/sliding_sync_ext.rs b/crates/matrix-sdk-ui/src/timeline/sliding_sync_ext.rs index 8a0e807ec..3b906d04f 100644 --- a/crates/matrix-sdk-ui/src/timeline/sliding_sync_ext.rs +++ b/crates/matrix-sdk-ui/src/timeline/sliding_sync_ext.rs @@ -14,7 +14,7 @@ use async_trait::async_trait; use matrix_sdk::SlidingSyncRoom; -use tracing::{error, instrument}; +use tracing::{error, instrument, warn}; use super::{EventTimelineItem, Timeline, TimelineBuilder}; @@ -23,11 +23,11 @@ pub trait SlidingSyncRoomExt { /// Get a `Timeline` for this room. async fn timeline(&self) -> Option; - /// Get the latest timeline item of this room. + /// Get the latest timeline item of this room, if it is already cached. /// /// Use `Timeline::latest_event` instead if you already have a timeline for /// this `SlidingSyncRoom`. - async fn latest_event(&self) -> Option; + fn latest_timeline_item(&self) -> Option; } #[async_trait] @@ -36,9 +36,12 @@ impl SlidingSyncRoomExt for SlidingSyncRoom { Some(sliding_sync_timeline_builder(self)?.track_read_marker_and_receipts().build().await) } + /// Get a timeline item representing the latest event in this room. + /// This method wraps latest_event, converting the event into an + /// EventTimelineItem. #[instrument(skip_all)] - async fn latest_event(&self) -> Option { - sliding_sync_timeline_builder(self)?.read_only().build().await.latest_event().await + fn latest_timeline_item(&self) -> Option { + self.latest_event().and_then(|e| EventTimelineItem::from_latest_event(self, e)) } } @@ -52,3 +55,130 @@ fn sliding_sync_timeline_builder(room: &SlidingSyncRoom) -> OptionMy msg", 122343); + process_event_via_sync(room_id, event, &client).await; + + // When we ask for the latest event in the room + let room = SlidingSyncRoom::new( + client.clone(), + room_id.to_owned(), + v4::SlidingSyncRoom::new(), + Vec::new(), + ); + let actual = room.latest_timeline_item().unwrap(); + + // Then it is wrapped as an EventTimelineItem + assert_eq!(actual.sender, user_id); + assert_matches!(actual.sender_profile, TimelineDetails::Unavailable); + assert_eq!(actual.timestamp.0, UInt::new(122343).unwrap()); + if let MessageType::Text(txt) = actual.content.as_message().unwrap().msgtype() { + assert_eq!(txt.body, "**My msg**"); + let formatted = txt.formatted.as_ref().unwrap(); + assert_eq!(formatted.format, MessageFormat::Html); + assert_eq!(formatted.body, "My msg"); + } else { + panic!("Unexpected message type"); + } + } + + async fn process_event_via_sync(room_id: &RoomId, event: SyncTimelineEvent, client: &Client) { + let mut room = v4::SlidingSyncRoom::new(); + room.timeline.push(event.event); + let response = response_with_room(room_id, room).await; + client.process_sliding_sync(&response).await.unwrap(); + } + + fn message_event( + room_id: &RoomId, + user_id: &UserId, + body: &str, + formatted_body: &str, + ts: u64, + ) -> SyncTimelineEvent { + SyncTimelineEvent::new( + Raw::from_json_string( + json!({ + "event_id": "$eventid6", + "sender": user_id, + "origin_server_ts": ts, + "type": "m.room.message", + "room_id": room_id.to_string(), + "content": { + "body": body, + "format": "org.matrix.custom.html", + "formatted_body": formatted_body, + "msgtype": "m.text" + }, + }) + .to_string(), + ) + .unwrap(), + ) + } + + async fn response_with_room(room_id: &RoomId, room: v4::SlidingSyncRoom) -> v4::Response { + let mut response = v4::Response::new("6".to_owned()); + response.rooms.insert(room_id.to_owned(), room); + response + } + + /// Copied from matrix_sdk_base::sliding_sync::test + async fn logged_in_client(homeserver_url: Option) -> Client { + let base_client = BaseClient::new(); + base_client + .set_session_meta(SessionMeta { + user_id: user_id!("@u:e.uk").to_owned(), + device_id: device_id!("XYZ").to_owned(), + }) + .await + .expect("Failed to set session meta"); + + test_client_builder(homeserver_url) + .request_config(RequestConfig::new().disable_retry()) + .base_client(base_client) + .build() + .await + .unwrap() + } + + fn test_client_builder(homeserver_url: Option) -> ClientBuilder { + let homeserver = homeserver_url.as_deref().unwrap_or("http://localhost:1234"); + Client::builder().homeserver_url(homeserver).server_versions([MatrixVersion::V1_0]) + } +} diff --git a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs index 98397981d..3b8b81486 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -2087,7 +2087,7 @@ async fn test_room_latest_event() -> Result<(), Error> { let room = room_list.room(room_id).await?; // The latest event does not exist. - assert!(room.latest_event().await.is_none()); + assert!(room.latest_event().is_none()); sync_then_assert_request_and_fake_response! { [server, room_list, sync] @@ -2107,7 +2107,7 @@ async fn test_room_latest_event() -> Result<(), Error> { // The latest event exists. assert_matches!( - room.latest_event().await, + room.latest_event(), Some(event) => { assert_eq!(event.event_id(), Some(event_id!("$x0:bar.org"))); } @@ -2131,7 +2131,7 @@ async fn test_room_latest_event() -> Result<(), Error> { // The latest event has been updated. assert_matches!( - room.latest_event().await, + room.latest_event(), Some(event) => { assert_eq!(event.event_id(), Some(event_id!("$x1:bar.org"))); } diff --git a/crates/matrix-sdk/src/client/builder.rs b/crates/matrix-sdk/src/client/builder.rs index 748e1ae7d..bb5b8297d 100644 --- a/crates/matrix-sdk/src/client/builder.rs +++ b/crates/matrix-sdk/src/client/builder.rs @@ -81,6 +81,7 @@ pub struct ClientBuilder { appservice_mode: bool, server_versions: Option>, handle_refresh_tokens: bool, + base_client: Option, } impl ClientBuilder { @@ -94,6 +95,7 @@ impl ClientBuilder { appservice_mode: false, server_versions: None, handle_refresh_tokens: false, + base_client: None, } } @@ -301,6 +303,13 @@ impl ClientBuilder { self } + /// Public for test only + #[doc(hidden)] + pub fn base_client(mut self, base_client: BaseClient) -> Self { + self.base_client = Some(base_client); + self + } + /// Create a [`Client`] with the options set on this builder. /// /// # Errors @@ -330,20 +339,24 @@ impl ClientBuilder { HttpConfig::Custom(c) => c, }; - #[allow(clippy::infallible_destructuring_match)] - let store_config = match self.store_config { - #[cfg(feature = "sqlite")] - BuilderStoreConfig::Sqlite { path, passphrase } => { - matrix_sdk_sqlite::make_store_config(&path, passphrase.as_deref()).await? - } - #[cfg(feature = "indexeddb")] - BuilderStoreConfig::IndexedDb { name, passphrase } => { - matrix_sdk_indexeddb::make_store_config(&name, passphrase.as_deref()).await? - } - BuilderStoreConfig::Custom(config) => config, + let base_client = if let Some(base_client) = self.base_client { + base_client + } else { + #[allow(clippy::infallible_destructuring_match)] + let store_config = match self.store_config { + #[cfg(feature = "sqlite")] + BuilderStoreConfig::Sqlite { path, passphrase } => { + matrix_sdk_sqlite::make_store_config(&path, passphrase.as_deref()).await? + } + #[cfg(feature = "indexeddb")] + BuilderStoreConfig::IndexedDb { name, passphrase } => { + matrix_sdk_indexeddb::make_store_config(&name, passphrase.as_deref()).await? + } + BuilderStoreConfig::Custom(config) => config, + }; + BaseClient::with_store_config(store_config) }; - let base_client = BaseClient::with_store_config(store_config); let http_client = HttpClient::new(inner_http_client.clone(), self.request_config); let mut authentication_server_info = None; diff --git a/crates/matrix-sdk/src/sliding_sync/client.rs b/crates/matrix-sdk/src/sliding_sync/client.rs index 52a0f174e..29b70a351 100644 --- a/crates/matrix-sdk/src/sliding_sync/client.rs +++ b/crates/matrix-sdk/src/sliding_sync/client.rs @@ -14,11 +14,9 @@ impl Client { Ok(SlidingSync::builder(id.into(), self.clone())?) } + /// Handle all the information provided in a sliding sync response #[instrument(skip(self, response))] - pub(crate) async fn process_sliding_sync( - &self, - response: &v4::Response, - ) -> Result { + pub async fn process_sliding_sync(&self, response: &v4::Response) -> Result { let response = self.base_client().process_sliding_sync(response).await?; debug!("done processing on base_client"); self.handle_sync_response(&response).await?; diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index c685b7f3e..f87374ac6 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -45,7 +45,7 @@ pub struct SlidingSyncRoom { impl SlidingSyncRoom { /// Create a new `SlidingSyncRoom`. - pub(super) fn new( + pub fn new( client: Client, room_id: OwnedRoomId, inner: v4::SlidingSyncRoom, @@ -125,6 +125,11 @@ impl SlidingSyncRoom { self.inner.client.clone() } + /// Find the latest event in this room + pub fn latest_event(&self) -> Option { + self.inner.client.get_room(&self.inner.room_id).and_then(|room| room.latest_event()) + } + pub(super) fn update( &mut self, room_data: v4::SlidingSyncRoom, From 3fd1542a257cf97f699e474bfdcd4e1894d65d76 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jul 2023 12:59:49 +0100 Subject: [PATCH 02/12] Fetch sender profile when returning latest event --- bindings/matrix-sdk-ffi/src/room_list.rs | 4 +- .../src/room_list_service/room.rs | 4 +- .../src/timeline/event_item/mod.rs | 92 +++++++++++++++++-- .../src/timeline/sliding_sync_ext.rs | 13 +-- 4 files changed, 96 insertions(+), 17 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index b9967f8af..4314cebc9 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -373,8 +373,8 @@ impl RoomListItem { self.inner.unsubscribe(); } - fn latest_event(&self) -> Option> { - self.inner.latest_event().map(EventTimelineItem).map(Arc::new) + async fn latest_event(&self) -> Option> { + self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new) } fn has_unread_notifications(&self) -> bool { diff --git a/crates/matrix-sdk-ui/src/room_list_service/room.rs b/crates/matrix-sdk-ui/src/room_list_service/room.rs index 052061049..daf2c9f80 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/room.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/room.rs @@ -135,8 +135,8 @@ impl Room { /// /// It's different from `Self::timeline().latest_event()` as it won't track /// the read marker and receipts. - pub fn latest_event(&self) -> Option { - self.inner.sliding_sync_room.latest_timeline_item() + pub async fn latest_event(&self) -> Option { + self.inner.sliding_sync_room.latest_timeline_item().await } /// Is there any unread notifications? diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index cead2d070..eeb9ac6f1 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -83,10 +83,12 @@ impl EventTimelineItem { /// If the supplied low-level SyncTimelineEventy is suitable for use as the /// latest_event in a message preview, wrap it as an EventTimelineItem, #[cfg(feature = "experimental-sliding-sync")] - pub(crate) fn from_latest_event( + pub(crate) async fn from_latest_event( room: &SlidingSyncRoom, sync_event: SyncTimelineEvent, ) -> Option { + use super::traits::RoomDataProvider; + let raw_sync_event = sync_event.event; let encryption_info = sync_event.encryption_info; @@ -135,10 +137,15 @@ impl EventTimelineItem { } .into(); - // If we need to sender profiles in the message previews, we will need to - // cache the contents of a Profile struct inside RoomInfo similar to how we - // are caching the event at the moment. - let sender_profile = TimelineDetails::Unavailable; + let room = room.client().get_room(room.room_id()); + let sender_profile = if let Some(room) = room { + room.profile(&sender) + .await + .map(TimelineDetails::Ready) + .unwrap_or(TimelineDetails::Unavailable) + } else { + TimelineDetails::Unavailable + }; Some(EventTimelineItem::new(sender, sender_profile, timestamp, item_content, event_kind)) } @@ -480,9 +487,9 @@ mod test { ); // When we construct a timeline event from it - let timeline_item = EventTimelineItem::from_latest_event(&room, event).unwrap(); + let timeline_item = EventTimelineItem::from_latest_event(&room, event).await.unwrap(); - // Then its properieis correctly translate + // Then its properties correctly translate assert_eq!(timeline_item.sender, user_id); assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable); assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap()); @@ -496,6 +503,77 @@ mod test { } } + #[async_test] + #[cfg(feature = "experimental-sliding-sync")] + async fn latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender() { + // Given a sync event that is suitable to be used as a latest_event, and a room + // with a member event for the sender + + use ruma::owned_mxc_uri; + let room_id = room_id!("!q:x.uk"); + let user_id = user_id!("@t:o.uk"); + let event = message_event(room_id, user_id, "**My M**", "My M", 122344); + let client = logged_in_client(None).await; + let mut room = v4::SlidingSyncRoom::new(); + room.timeline.push(member_event(room_id, user_id, "Alice Margatroid", "mxc://e.org/SEs")); + let ss_room = + SlidingSyncRoom::new(client.clone(), room_id.to_owned(), room.clone(), Vec::new()); + + // And the room is stored in the client so it can be extracted when needed + let response = response_with_room(room_id, room).await; + client.process_sliding_sync(&response).await.unwrap(); + + // When we construct a timeline event from it + let timeline_item = EventTimelineItem::from_latest_event(&ss_room, event).await.unwrap(); + + // Then its sender is properly populated + let profile = assert_matches!(timeline_item.sender_profile, TimelineDetails::Ready(p) => p); + assert_eq!( + profile, + Profile { + display_name: Some("Alice Margatroid".to_owned()), + display_name_ambiguous: false, + avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs")) + } + ); + } + + fn member_event( + room_id: &RoomId, + user_id: &UserId, + display_name: &str, + avatar_url: &str, + ) -> Raw { + Raw::from_json_string( + json!({ + "type": "m.room.member", + "content": { + "avatar_url": avatar_url, + "displayname": display_name, + "membership": "join", + "reason": "" + }, + "event_id": "$143273582443PhrSn:example.org", + "origin_server_ts": 143273583, + "room_id": room_id, + "sender": "@example:example.org", + "state_key": user_id, + "type": "m.room.member", + "unsigned": { + "age": 1234 + } + }) + .to_string(), + ) + .unwrap() + } + + async fn response_with_room(room_id: &RoomId, room: v4::SlidingSyncRoom) -> v4::Response { + let mut response = v4::Response::new("6".to_owned()); + response.rooms.insert(room_id.to_owned(), room); + response + } + fn message_event( room_id: &RoomId, user_id: &UserId, diff --git a/crates/matrix-sdk-ui/src/timeline/sliding_sync_ext.rs b/crates/matrix-sdk-ui/src/timeline/sliding_sync_ext.rs index 3b906d04f..bbfa1c0e0 100644 --- a/crates/matrix-sdk-ui/src/timeline/sliding_sync_ext.rs +++ b/crates/matrix-sdk-ui/src/timeline/sliding_sync_ext.rs @@ -14,7 +14,7 @@ use async_trait::async_trait; use matrix_sdk::SlidingSyncRoom; -use tracing::{error, instrument, warn}; +use tracing::{error, instrument}; use super::{EventTimelineItem, Timeline, TimelineBuilder}; @@ -27,7 +27,7 @@ pub trait SlidingSyncRoomExt { /// /// Use `Timeline::latest_event` instead if you already have a timeline for /// this `SlidingSyncRoom`. - fn latest_timeline_item(&self) -> Option; + async fn latest_timeline_item(&self) -> Option; } #[async_trait] @@ -40,8 +40,9 @@ impl SlidingSyncRoomExt for SlidingSyncRoom { /// This method wraps latest_event, converting the event into an /// EventTimelineItem. #[instrument(skip_all)] - fn latest_timeline_item(&self) -> Option { - self.latest_event().and_then(|e| EventTimelineItem::from_latest_event(self, e)) + async fn latest_timeline_item(&self) -> Option { + let latest_event = self.latest_event()?; + EventTimelineItem::from_latest_event(self, latest_event).await } } @@ -82,7 +83,7 @@ mod tests { let room = SlidingSyncRoom::new(client, room_id, v4::SlidingSyncRoom::new(), Vec::new()); // When we ask for the latest event, it is None - assert!(room.latest_timeline_item().is_none()); + assert!(room.latest_timeline_item().await.is_none()); } #[async_test] @@ -101,7 +102,7 @@ mod tests { v4::SlidingSyncRoom::new(), Vec::new(), ); - let actual = room.latest_timeline_item().unwrap(); + let actual = room.latest_timeline_item().await.unwrap(); // Then it is wrapped as an EventTimelineItem assert_eq!(actual.sender, user_id); From a2ca16857348c86fb2a3ce21c7ce5250fda1f6a4 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jul 2023 13:22:28 +0100 Subject: [PATCH 03/12] Provide path for a doc link --- crates/matrix-sdk-base/src/latest_event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 3efd6b7b8..7f8ba81c2 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -1,5 +1,5 @@ //! Utilities for working with events to decide whether they are suitable for -//! use as a [RoomInfo::latest_event]. +//! use as a [crate::RoomInfo::latest_event]. use ruma::events::{ room::message::RoomMessageEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, From 82c7a05f01f3a77d02600ee25827fce1dd620012 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jul 2023 15:05:48 +0100 Subject: [PATCH 04/12] Fixes for problems found through CI --- bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js | 4 ++-- crates/matrix-sdk-base/src/latest_event.rs | 3 ++- crates/matrix-sdk-base/src/rooms/normal.rs | 6 ++++-- crates/matrix-sdk-base/src/sliding_sync.rs | 3 ++- crates/matrix-sdk-ui/tests/integration/room_list_service.rs | 6 +++--- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js index cfe4758f6..41a0d3ed0 100644 --- a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js @@ -139,7 +139,7 @@ describe(OlmMachine.name, () => { await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys), ); - expect(receiveSyncChanges).toEqual([]); + expect(receiveSyncChanges).toEqual([[],[]]); }); test("can get the outgoing requests that need to be send out", async () => { @@ -153,7 +153,7 @@ describe(OlmMachine.name, () => { await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys), ); - expect(receiveSyncChanges).toEqual([]); + expect(receiveSyncChanges).toEqual([[], []]); const outgoingRequests = await m.outgoingRequests(); diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index 7f8ba81c2..cf61e333f 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -1,6 +1,8 @@ //! Utilities for working with events to decide whether they are suitable for //! use as a [crate::RoomInfo::latest_event]. +#![cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] + use ruma::events::{ room::message::RoomMessageEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent, SyncMessageLikeEvent, @@ -28,7 +30,6 @@ pub enum PossibleLatestEvent<'a> { /// Decide whether an event could be stored as the latest event in a room. /// Returns a LatestEvent representing our decision. -#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] pub fn is_suitable_for_latest_event(event: &AnySyncTimelineEvent) -> PossibleLatestEvent<'_> { match event { // Suitable - we have an m.room.message that was not redacted diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 493ea8ee2..8d6f791c9 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -19,6 +19,7 @@ use std::{ use bitflags::bitflags; use futures_util::stream::{self, StreamExt}; +#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer}; use ruma::{ api::client::sync::sync_events::v3::RoomSummary as RumaSummary, @@ -37,14 +38,15 @@ use ruma::{ tombstone::RoomTombstoneEventContent, }, tag::Tags, - AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, + AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, RoomAccountDataEventType, }, room::RoomType, - serde::Raw, EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId, }; +#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] +use ruma::{events::AnySyncTimelineEvent, serde::Raw}; use serde::{Deserialize, Serialize}; use tracing::{debug, info, instrument, warn}; diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index d47c91500..3553525a7 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -27,12 +27,13 @@ use ruma::{ use tracing::{debug, info, instrument, warn}; use super::BaseClient; +#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] +use crate::latest_event::{is_suitable_for_latest_event, PossibleLatestEvent}; #[cfg(feature = "e2e-encryption")] use crate::RoomMemberships; use crate::{ deserialized_responses::AmbiguityChanges, error::Result, - latest_event::{is_suitable_for_latest_event, PossibleLatestEvent}, rooms::RoomState, store::{ambiguity_map::AmbiguityCache, StateChanges, Store}, sync::{JoinedRoom, Rooms, SyncResponse}, diff --git a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs index 3b8b81486..98397981d 100644 --- a/crates/matrix-sdk-ui/tests/integration/room_list_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/room_list_service.rs @@ -2087,7 +2087,7 @@ async fn test_room_latest_event() -> Result<(), Error> { let room = room_list.room(room_id).await?; // The latest event does not exist. - assert!(room.latest_event().is_none()); + assert!(room.latest_event().await.is_none()); sync_then_assert_request_and_fake_response! { [server, room_list, sync] @@ -2107,7 +2107,7 @@ async fn test_room_latest_event() -> Result<(), Error> { // The latest event exists. assert_matches!( - room.latest_event(), + room.latest_event().await, Some(event) => { assert_eq!(event.event_id(), Some(event_id!("$x0:bar.org"))); } @@ -2131,7 +2131,7 @@ async fn test_room_latest_event() -> Result<(), Error> { // The latest event has been updated. assert_matches!( - room.latest_event(), + room.latest_event().await, Some(event) => { assert_eq!(event.event_id(), Some(event_id!("$x1:bar.org"))); } From c9a11a58bff4834b9e59493a7dee14956ec37d6e Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jul 2023 15:11:00 +0100 Subject: [PATCH 05/12] Format JavaScript --- bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js index 41a0d3ed0..6c2024895 100644 --- a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js @@ -139,7 +139,7 @@ describe(OlmMachine.name, () => { await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys), ); - expect(receiveSyncChanges).toEqual([[],[]]); + expect(receiveSyncChanges).toEqual([[], []]); }); test("can get the outgoing requests that need to be send out", async () => { From b525491bd53af31355f9d1706cdcee5f9f8a72c0 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jul 2023 15:20:41 +0100 Subject: [PATCH 06/12] Fix warnings from CI --- crates/matrix-sdk-base/src/client.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 1b334b7f9..dea15883c 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -563,7 +563,8 @@ impl BaseClient { changed_devices: &api::sync::sync_events::DeviceLists, one_time_keys_counts: &BTreeMap, unused_fallback_keys: Option<&[ruma::DeviceKeyAlgorithm]>, - changes: &mut StateChanges, + #[cfg(feature = "experimental-sliding-sync")] changes: &mut StateChanges, + #[cfg(not(feature = "experimental-sliding-sync"))] _changes: &mut StateChanges, ) -> Result>> { if let Some(o) = self.olm_machine().await.as_ref() { // Let the crypto machine handle the sync response, this @@ -585,6 +586,8 @@ impl BaseClient { self.decrypt_latest_events(&mut room, changes).await; } } + #[cfg(not(feature = "experimental-sliding-sync"))] + drop(room_key_updates); // Silence unused variable warning Ok(events) } else { From 31e9572d4248e8b115766987834180da29014cbb Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jul 2023 15:22:45 +0100 Subject: [PATCH 07/12] Allow unused code for read_only --- crates/matrix-sdk-ui/src/timeline/builder.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/matrix-sdk-ui/src/timeline/builder.rs b/crates/matrix-sdk-ui/src/timeline/builder.rs index 86bbef3bb..e7703d61c 100644 --- a/crates/matrix-sdk-ui/src/timeline/builder.rs +++ b/crates/matrix-sdk-ui/src/timeline/builder.rs @@ -71,6 +71,7 @@ impl TimelineBuilder { /// /// This improves efficiency a little bit since no background task will be /// spawned for sending messages. + #[allow(dead_code)] pub(crate) fn read_only(mut self) -> Self { self.read_only = true; self From 9bcc1e9e00175fa9a768b267b81c8fc9100bd5ec Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jul 2023 15:30:59 +0100 Subject: [PATCH 08/12] More JavaScript test updates --- bindings/matrix-sdk-crypto-js/tests/machine.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 220d105a8..6abd4bde2 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -217,7 +217,7 @@ describe(OlmMachine.name, () => { await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys), ); - expect(receiveSyncChanges).toEqual([]); + expect(receiveSyncChanges).toEqual([[], []]); }); test("can receive sync changes with unusedFallbackKeys as undefined", async () => { @@ -230,7 +230,7 @@ describe(OlmMachine.name, () => { await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, undefined), ); - expect(receiveSyncChanges).toEqual([]); + expect(receiveSyncChanges).toEqual([[], []]); }); test("can get the outgoing requests that need to be send out", async () => { @@ -244,7 +244,7 @@ describe(OlmMachine.name, () => { await m.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys), ); - expect(receiveSyncChanges).toEqual([]); + expect(receiveSyncChanges).toEqual([[], []]); const outgoingRequests = await m.outgoingRequests(); From 6e84f969ad3f69605ec07024ca9e711d3ed39e51 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jul 2023 15:42:52 +0100 Subject: [PATCH 09/12] More feature flag fiddling --- crates/matrix-sdk-base/src/rooms/normal.rs | 10 ++++++---- crates/matrix-sdk-base/src/sliding_sync.rs | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 8d6f791c9..e8b70ddfa 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -19,8 +19,10 @@ use std::{ use bitflags::bitflags; use futures_util::stream::{self, StreamExt}; +#[cfg(all(feature = "experimental-sliding-sync"))] +use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] -use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer}; +use matrix_sdk_common::ring_buffer::RingBuffer; use ruma::{ api::client::sync::sync_events::v3::RoomSummary as RumaSummary, events::{ @@ -79,7 +81,7 @@ pub struct Room { /// not sure whether holding too many of them might make the cache too /// slow to load on startup. Keeping them here means they are not cached /// to disk but held in memory. - #[cfg(feature = "experimental-sliding-sync")] + #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] pub latest_encrypted_events: Arc>>>, } @@ -125,7 +127,7 @@ impl From<&MembershipState> for RoomState { impl Room { /// The size of the latest_encrypted_events RingBuffer - #[cfg(feature = "experimental-sliding-sync")] + #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] const MAX_ENCRYPTED_EVENTS: usize = 10; pub(crate) fn new( @@ -148,7 +150,7 @@ impl Room { room_id: room_info.room_id.clone(), store, inner: Arc::new(SyncRwLock::new(room_info)), - #[cfg(feature = "experimental-sliding-sync")] + #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new( Self::MAX_ENCRYPTED_EVENTS, ))), diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 3553525a7..4df71e4f5 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -250,6 +250,7 @@ impl BaseClient { // Cache the latest decrypted event in room_info, and also keep any later // encrypted events, so we can slot them in when we get the keys. + #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] cache_latest_events(&mut room, &mut room_info, &timeline.events); #[cfg(feature = "e2e-encryption")] @@ -373,6 +374,7 @@ impl BaseClient { /// Find the most recent decrypted event and cache it in the supplied RoomInfo. /// If any encrypted events are found after that one, store them in the RoomInfo /// too so we can use them when we get the relevant keys. +#[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] fn cache_latest_events(room: &mut Room, room_info: &mut RoomInfo, events: &[SyncTimelineEvent]) { let mut encrypted_events = Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity()); From 96098e8f8b5233426da74c34afa770bdeb3f8445 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jul 2023 15:44:16 +0100 Subject: [PATCH 10/12] More feature flag fiddling --- crates/matrix-sdk-base/src/rooms/normal.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index e8b70ddfa..297a614b5 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -1523,6 +1523,7 @@ mod test { .push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap()); } + #[cfg(feature = "experimental-sliding-sync")] fn make_event(event_id: &str) -> SyncTimelineEvent { SyncTimelineEvent::new( Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(), From ff0cd7c592d987486d0850994ccb11ce0b1973ca Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jul 2023 15:55:29 +0100 Subject: [PATCH 11/12] Fix one more JS test --- bindings/matrix-sdk-crypto-js/tests/helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/matrix-sdk-crypto-js/tests/helper.js b/bindings/matrix-sdk-crypto-js/tests/helper.js index eaf58d5b8..5aa3e1442 100644 --- a/bindings/matrix-sdk-crypto-js/tests/helper.js +++ b/bindings/matrix-sdk-crypto-js/tests/helper.js @@ -20,7 +20,7 @@ async function addMachineToMachine(machineToAdd, machine) { await machineToAdd.receiveSyncChanges(toDeviceEvents, changedDevices, oneTimeKeyCounts, unusedFallbackKeys), ); - expect(receiveSyncChanges).toEqual([]); + expect(receiveSyncChanges).toEqual([[], []]); const outgoingRequests = await machineToAdd.outgoingRequests(); From bac555d31b3de595d36a39064b07e7302a386ba9 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jul 2023 16:19:38 +0100 Subject: [PATCH 12/12] More CI fixes --- crates/matrix-sdk-base/src/rooms/normal.rs | 4 ++-- crates/matrix-sdk-base/src/sliding_sync.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index 297a614b5..ba22bfa30 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -19,7 +19,7 @@ use std::{ use bitflags::bitflags; use futures_util::stream::{self, StreamExt}; -#[cfg(all(feature = "experimental-sliding-sync"))] +#[cfg(feature = "experimental-sliding-sync")] use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] use matrix_sdk_common::ring_buffer::RingBuffer; @@ -391,7 +391,7 @@ impl Room { /// to decrypt these, the most recent relevant one will replace /// latest_event. (We can't tell which one is relevant until /// they are decrypted.) - #[cfg(feature = "experimental-sliding-sync")] + #[cfg(all(feature = "e2e-encryption", feature = "experimental-sliding-sync"))] pub(crate) fn latest_encrypted_events(&self) -> Vec> { self.latest_encrypted_events.read().unwrap().iter().cloned().collect() } diff --git a/crates/matrix-sdk-base/src/sliding_sync.rs b/crates/matrix-sdk-base/src/sliding_sync.rs index 4df71e4f5..aca78b0ac 100644 --- a/crates/matrix-sdk-base/src/sliding_sync.rs +++ b/crates/matrix-sdk-base/src/sliding_sync.rs @@ -200,6 +200,7 @@ impl BaseClient { let required_state = Self::deserialize_events(&room_data.required_state); // Find or create the room in the store + #[allow(unused_mut)] // Required for some feature flag combinations let (mut room, mut room_info, invited_room) = self.process_sliding_sync_room_membership( room_data, &required_state,